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.
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
Currently public interception interfaces exist for DbCommand execution and DbCommandTree creation.
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.
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
- 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
Luckily this kind of situation is rare and will likely not be an issue for most applications.
The DbInterceptionContext object that is passed to the interceptor methods contains Result, OriginalResult, Exception, and OriginalException properties. These properties are set to null/zero for calls to the interception methods that are called before the
operation is executed—e.g. the …Executing methods. If the operation is executed and succeeds, then Result and OriginalResult are set to the result of the operation for calls to interception methods that are called after the operation is executed—e.g.
the …Executed methods. Likewise, if the operation throws, then Exception and OriginalException will be set.
Note that not all interception contexts will expose all these properties, only the ones that are applicable to the operation being intercepted.
Suppression of execution
A ”before” interceptor can suppress the execution of the operation that is being intercepted. That is, the interceptor can tell EF not to execute the operation but to instead return some other result. This is done by setting the Result property
on interception context in the “before” interceptor.
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.
In a similar way, a “before” interceptor can set the Exception property to tell EF to throw the given exception instead of executing the operation.
Note that the OriginalResult and OriginalException properties are read-only and are only set by EF. OriginalResult will be set to the actual result of executing the operation unless it threw or execution was suppressed, in which case it will be null/default.
Likewise, OriginalException is only set if the operation was actually executed and threw.
Changing Result after execution
If an interceptor sets the Result property after the command has executed (e.g. in one of the …Executed methods) then EF will use the changed result instead of the result that was actually returned from the operation. Likewise, if the Exception property
is set after execution, then EF will throw the given exception instead of returning the result.
Once a class that implements one or more of the interception interfaces has been created it can be registered with EF using the DbInterception class. Note that interceptors can be added or removed at any time. For example:
It is also possible to register interceptors in DbConfiguration (part of the new
code-based configuration feature).
public class MyConfig : DbConfiguration
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:
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 suppressed execution 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. For example, to send output to the Console use something like:
using (var context = new BlogContext())
context.Database.Log = Console.Write;
// Your code here...
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
- 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
Under the covers the Database.Log property makes use of a DatabaseLogFormatter object. This object effectively binds a IDbCommandInterceptor implementation to a delegate that accepts strings and a DbContext. This means that interception methods on DatabaseLogFormatter
are called before and after the execution of commands by EF. These DatabaseLogFormatter 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 DatabaseLogFormatter class has been created it needs to be registered with EF. This is done using
code-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 SetDatabaseLogFormatter in the
constructor of this new class.