This project is read-only.

Design Meeting Notes - May 9, 2013

Interception building blocks

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.

An overview of the interception feature will be posted to the feature specifications page. The notes below do not cover the entire feature but rather the points discussed in the design meeting where a change was considered.

Interfaces and  base classes

Currently the plan is to have one interface per type that can be intercepted. So, for example, there is currently an IDbCommandInterceptor and and IDbCommandTreeInterceptor, and in the future there might be an IDbConnectionInterceptor. There is also currently a single base type that implements all the public interception interfaces.

Question: would it be simpler to understand and use if there were a single interface or base type and no interfaces?

The current separation was done for several reasons:

  • The design minimizes perf impact for code that needs to intercept for one type but not another. This is because if no interceptor is registered for a given type then EF doesn’t need to make any calls for that type. With a single type EF has no way of knowing whether or not a call into the interceptor is required for a given type and so would always call.
    • This raises the question of whether or not we should even provide a base class that implements all the interfaces
  • This helps prevent breaking changes compared to a single interface that we would not be able to add to without it being a breaking change.
    • A single base type would not have this issue, but then no interceptor would be able to have any other base type
  • The interfaces factor related interceptions together into manageable groups

Decisions:

  • We will will continue to have interfaces organized like this
  • We will remove the single base class since it promotes a bad pattern and instead of base classes for any interception interface with many methods

Other interceptors for EF6

Given where we are in the development cycle for EF6 should we consider adding any additional interceptors? In particular, a LINQ expression tree interceptor? Decision: we will do this post EF6.

Related to this, are there other things that it is worth intercepting on DbCommand?

  • Interception of properties doesn’t seem valuable
  • For other methods:
    • Cancel is never called by EF
    • CreateParameter is only called when creating parameters for a raw SQL query, and parameters can be examined and manipulated for execution anyway
    • Prepare is never called by EF
    • Dispose for a command doesn’t seem to be valuable to intercept

Decision: we will not add anything here

Filtering design

Currently all interceptors are registered globally. This means that interception for a specific DbContext or ObjectContext requires that the interceptor filter for the context. If many contexts in a single app domain are all doing this it might have a perf impact. However this is speculative and has not been demonstrated.

A solution to this could be to include the DbContext and/or ObjectContext in the interceptor registration.

Decision: we will not do this at this time because:

  • If it turns out that this really is an issue then we can make this change in the future without it being a breaking change
  • A more appropriate pattern for an application that needs to intercept for multiple contexts would be to create a single interceptor to do this
    • This is also more appropriate because it allows efficient filtering by context type, which is often more useful than context instance in these situations
  • If this is never needed, then adding anyway adds to design and API complexity and concept count

Collapse everything into the interception context

We discussed the idea of collapsing all the methods together on IDbCommandInterceptor and putting all information (DbCommand, result, method being intercepted, etc.) into the context. However, the general feeling was that keeping the object being intercepted (DbCommand in this case) as first class was worthwhile, and that collapsing all the methods would make the interface harder to understand and could result in a lot of switch-like code.

Result handling and operation overrides

Currently the “Executed” interception methods on IDbCommandInterceptor receive a parameter that is the result of executing the operation. These methods then return a value which should normally be this result. However, the interceptor can change the result by returning a different value. This can be useful in situations where the provider returns a result such as 0 rows affected but the interceptor knows that really the correct thing happened and wants to return 1 to EF so that EF does not throw a concurrency exception.

The problem with this pattern is that when implementing the interface it is not immediately obvious what the return value from the method is for or what should be returned.

Related to this is that it would be useful to allow the interceptor to tell EF not to actually perform the operation. For example, consider a wrapping provider that is attempting to batch. One approach for this is to collect all the commands but then hold off on executing them immediately. For this to be possible with interception the interceptor needs to be able to tell EF not to execute the commands. Note that this is similar but different from cancelation since EF should continue as though the operation was executed using whatever result the interceptor provides.

Two possible solutions to these problems will be investigated:

  • Allow the interception context to be mutated such that preventing command execution and result modification can be done by changing the interception context
  • Provide another, mutable object that allows result modification and execution cancelation

We also considered using ref parameters, but the general feeling was that we should avoid doing this if possible.

Related to this is that, where possible, we should use a more specific name for the result, such as “rowsAffected”.

DbContext.Database.Log

Built on top of the interception building blocks is a LINQ to SQL-like “Log” property that can be set to a TextWriter. For example, code like this:

using (var context = new BlogContext())
{
    context.Database.Log = Console.Out;

    var blog = context.Blogs.First(b => b.Title == "One Unicorn");

    blog.Posts.First().Title = "Green Eggs and Ham";

    blog.Posts.Add(new Post { Title = "I do not like them!"});

    context.SaveChangesAsync().Wait();
}

Will output something like this to the console:

SELECT TOP (1)
[Extent1].[Id] AS [Id],
[Extent1].[Title] AS [Title]
FROM [dbo].[Blogs] AS [Extent1]
WHERE (N'One Unicorn' = [Extent1].[Title]) AND ([Extent1].[Title] IS NOT NULL)
-- Completed with result: SqlDataReader

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Title] AS [Title],
[Extent1].[BlogId] AS [BlogId]
FROM [dbo].[Posts] AS [Extent1]
WHERE [Extent1].[BlogId] = @EntityKeyValue1
-- EntityKeyValue1: Input Nullable Int32 (Size = 0; Precision = 0; Scale = 0) [1]
-- Completed with result: SqlDataReader

update [dbo].[Posts]
set [Title] = @0
where ([Id] = @1)
-- @0: Input Nullable String (Size = -1; Precision = 0; Scale = 0) [Green Eggs and Ham]
-- @1: Input Nullable Int32 (Size = 0; Precision = 0; Scale = 0) [1]
-- Executing asynchronously
-- Completed with result: 1

insert [dbo].[Posts]([Title], [BlogId])
values (@0, @1)
select [Id]
from [dbo].[Posts]
where @@rowcount > 0 and [Id] = scope_identity()
-- @0: Input Nullable String (Size = -1; Precision = 0; Scale = 0) [I do not like them!]
-- @1: Input Nullable Int32 (Size = 0; Precision = 0; Scale = 0) [1]
-- Executing asynchronously
-- Completed with result: SqlDataReader

TextWriter or something else?

Should the property be a TextWriter, an event, or delegate? The advantage of TextWriter is that it is really simple to use and is a pattern that was introduced and well-liked in LINQ to SQL. It is worth keeping in mind that this is intended to be a really simple way to Log—full power is provided by the interception building blocks. An event could be used, but multi-cast is not required and, for this simple case, neither are event args, sender, etc. Changing the TextWriter to a delegate would not add significantly to how hard it is to use especially given that use of lambdas is now a relatively well understood pattern.

Decision: we will investigate using a delegate instead.

Output format

The default output format (which is easy to change) was discussed with the following conclusions:

  • Parameters will not be inlined by default, but we will consider creating a DbInliningCommandLogger which can be switched in for those who want executable SQL
    • The reason for this is that parameter information is useful and this is lost using a simple inlining approach
    • A simple inlining approach also means that what is logged is not actually what is sent to the database
    • Adding code to set and create parameters as comments would be nice, but it would be both quite verbose and require provider changes to do in a provider-agnostic way
  • A timestamp will be included before the command is executed and a time-elapsed value will be included after the command has executed
  • The parameter value will be moved to the start of the parameter output
  • Parameter facets (precision, scale, etc.) will only be included if they have non-default values

Last edited May 10, 2013 at 12:24 AM by ajcvickers, version 2