Interception
Goals
The high-level goal for the interception feature is to allow external code to observe and potentially intercept EF operations. The specific goal for EF6 is to allow generated SQL to be logged without using a wrapping provider. A sub-goal to this is to provide a simple way for applications to log generated SQL.
Interception interfaces
The interception code is built around the concept of interception interfaces. These interfaces inherit from IDbInterceptor and define methods that are called when EF performs some action. The intent is to have one interface per type of object being intercepted. For example, the IDbCommandInterceptor interface defines methods that are called before EF makes a call to ExecuteNonQuery, ExecuteScalar, ExecuteReader, and related methods. Likewise, the interface defines methods that are called when each of these operations completes.
Currently public interception interfaces exist for DbCommand and DbCommandTree.
The interception context
Looking at the methods defined on any of the interceptor interfaces it is apparent that every call is given an object of type DbInterceptionContext or some type derived from this such as DbCommandInterceptionContext<>. This object contains contextual information about the action that EF is taking. For example, if the action is being taken on behalf of a DbContext, then the DbContext is included in the DbInterceptionContext. Similarly, for commands that are being executed asynchronously, the IsAsync flag is set on DbCommandInterceptionContext.
Collecting this information together into an object keeps the interface methods relatively simple and allows new contextual information to be added in the future with it being a breaking change on the interface.
Caveat
It’s worth noting that the interception context is a best effort to provide contextual information. However, in some corner cases some information that you would expect to be there may not be there. This is because EF has code paths that cannot easily be changed and do not include information that might be expected. For example, when EF makes a call into a provider, the provider has no knowledge of the DbContext being used. If that provider, outside of EF, decides to call ExecuteNonQuery, then two things might happen:
- First the provider may just make the call directly, avoiding EF interception completely. (This is a consequence of having interception at the EF level rather than lower in the stack. It would be great if interception were lower in the stack, but this is unfortunately outside of the control of the EF team.)
- If the provider is aware of EF interception then it can dispatch the ExecuteNonQuery call through EF interceptors. This means that any registered interceptor will be notified and can act appropriately. This is what the SQL Server and SQL Server Compact providers do. However, even when a provider does this it is likely that the DbContext being used will not be included in the interception context because the provider has no knowledge of it, and a change to allow this would break the well-defined provider APIs.
Luckily this kind of situation is rare and will likely not be an issue for most applications.
Result handling
The generic DbCommandInterceptionContext<> class contains a property called “Result”. This property is set to null/zero for calls to the …Executing methods and is then set to the result of the operation for the calls to the …Executed methods. This means, that in simple scenarios such as logging the result can be be observed and logged.
Changing Result before execution
If an interceptor sets the Result property before the command has executed (in one of the …Executing methods) then EF will not attempt to actually executed the command. Instead the result set will be used directly by EF.
An example of how this might be used is the command batching that has traditionally been done with a wrapping provider. The interceptor would store the command for later execution as a batch but would “pretend” to EF that the command had executed as normal. Note that it requires more than this to implement batching, but this is an example of how changing the interception result might be used.
Changing Result after execution
If an interceptor sets the Result property after the command has executed (in one of the …Executed methods) then EF will use the changed result instead of the result that was actually returned from the operation.
Registering interceptors
Once a class that implements one or more of the interception interfaces has been created it can be registered with EF using the Interception class. For example:
Interception.AddInterceptor(new NLogCommandInterceptor());
Dispatching
In addition to methods for the registration of interceptors, the Interception class also has a Dispatch method. This method allows code that is not part of EF to dispatch notifications to interceptors on behalf of EF. This is the mechanism mentioned above that allows providers to let interceptors know that that a command is being executed outside of the control of EF. It would be rare for an application developer to ever need to use the Dispatch API, but in sure rare cases the calls would look like this:
Interception.Dispatch.Command.AsyncNonQuery( myCommand, new DbCommandInterceptionContext<int>());
This line of code will do the following:
- Make sure that IsAsync is set on the interception context
- Call NonQueryExecuting on all registered IDbCommandInterceptors
- Call ExecuteNonQueryAsync on the given command, unless one of the NonQueryExecuting methods set the Result property as described above
- Setup continuations on the async task such that NonQueryExecuted is called on all registered IDbCommandInterceptors
- Make sure that the result task contains the correct value, which may have been changed by one of the interceptors
Context Log property
The DbContext.Database.Log property can be set to a delegate for any method that takes a string. In particular, it can be used with any TextWriter by setting it to the “Write” method of that TextWriter. All SQL generated by the current context will be logged
to that writer.
When the Log property is set all of the following will be logged:
- SQL for all different kinds of commands. For example:
- Queries, including normal LINQ queries, eSQL queries, and raw queries from methods such as SqlQuery
- Inserts, updates, and deletes generated as part of SaveChanges
- Relationship loading queries such as those generated by lazy loading
- Parameters
- Whether or not the command is being executed asynchronously
- A timestamp indicating when the command started executing
- Whether or not the command completed successfully, failed by throwing an exception, or, for async, was canceled
- Some indication of the result value
- The approximate amount of time it took to execute the command
DbCommandLogger
Under the covers the Database.Log property makes use of a DbCommandLogger object. This object effectively binds a IDbCommandInterceptor implementation to a delegate that accepts strings and a DbContext. This means that interception methods on DbCommandLogger are called before and after the execution of commands by EF. These DbCommandLogger methods gather and format log output and send it to the delegate.
Changing what is logged and how it is formatted can be achieved by creating a new class that derives from DbCommandLogger and then overriding methods as appropriate. The most common methods to override are:
- LogCommand – Override this to change how commands are logged before they are executed. By default LogCommand calls LogParameter for each parameter; you may choose to do the same in your override or handle parameters differently instead.
- LogResult – Override this to change how the outcome from executing a command is logged.
- LogParameter – Override this to change the formatting and content of parameter logging.
Once a new DbCommandLogger class has been created it needs to be registered with EF. This is done usingcode-based configuration. In a nutshell this means creating a new class that derives from DbConfiguration in the same assembly as your DbContext class and then calling SetCommandLogger in the constructor of this new class.