Click here to Skip to main content
15,867,594 members
Articles / Database Development / SQL Server

Profiling Entity Framework 5 in code

Rate me:
Please Sign up or sign in to vote.
4.83/5 (9 votes)
27 Nov 2012CPOL4 min read 56.1K   693   39   12
Hook SaveChanges and see exactly what T-SQL is hitting your database without attaching a profiler

Introduction

This article will introduce you to hooking into the EntityFramework OnSaveChanges event which is buried deep in the framework. It’ll also explain how you can extract the T-SQL which is actually committed when SaveChanges is called.

Many a times I have attached a profiler to my database to see what EF is actually committing. The lack of functionality to do this in code disappoints me so I decided to write an extension to EntityFramework 5 to do just this.

The extension exposes a method on DbContext called HookSaveChanges(). This allows you to provide a callback function to be invoked each time save changes is called on you DbContext.

Using the code

Before I go into the detail of how the extension works, I’ll provide a simple example of usage. So if you’re not interested in how it works. Just study the code below, download the extension and plug it in.

First of all, you’ll need a database context and a table, here’s a very simple sample I’ll be using:

C#
public class TestContext : DbContext 
{
    public TestContext()
        : base(Properties.Settings.Default.TestContext)
    {
        Database.SetInitializer<TestContext>(null);
    }

    public DbSet<TestTable> TestTables { get; set; }

}

[Table("Test")]
public class TestTable
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Col1 { get; set; }

    [MaxLength(50)]
    public string Col2 { get; set; }
}

And here is the code required to hook the save changes event.

C#
class Program
{
    private static void Main(string[] args)
    {
        //create a new instance of your context
        var context = new TestContext();
        //call HookSaveChanges extension method
        context.HookSaveChanges(FuncDelegate);
        //do some db changes
        var tt = new TestTable() { Col2 = "Testing 123" };
        context.TestTables.Add(tt);
        //call save changes
        context.SaveChanges();
    }
    private static void FuncDelegate(DbContext dbContext, string command)
    {
        Console.WriteLine(command);
        Console.ReadLine();
    }
}

If you run the above code in a console application, you should see the following output:

Image 1

Detail

The best way to describe how the hook works is to walk you through the code in the order that I built it. The first thing I had to do was establish if such an event existed. By looking through reflected code (no need to download source), I found that the ObjectContext contains a public event called SavingChanges. This event if invoked at the very beginning of the SaveChanges method. Unfortunately, the ObjectContext is buried a few layers down the call chain from the DbContext. In order to reach this instance and attach a delegate to it, we need to chain through the call via reflection.

First up is the InternalContext which is declared on the DbContext as this:

C#
internal virtual InternalContext InternalContext {get;}

so, with a bit of type reflection magic we can just ignore the fact that it’s internal and grab the value:

C#
var internalContext = _context.GetType()
                               .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
                               .Where(p => p.Name == "InternalContext")
                               .Select(p => p.GetValue(_context,null))
                               .SingleOrDefault();

Ok, that’s the InternalContext sorted, next up is the ObjectContext (which is the one we’re interested in). Same again with the one difference being this property is marked as public:

C#
var objectContext = internalContext.GetType()
                           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                           .Where(p => p.Name == "ObjectContext")
                           .Select(p => p.GetValue(internalContext,null))
                           .SingleOrDefault();

Now that we have the ObjectContext instance we can grab the event that we’re interested in SavingChanges. Similar the code above, this is extracted like so:

C#
var saveChangesEvent = objectContext.GetType()
                                .GetEvents(BindingFlags.Public | BindingFlags.Instance)
                                .SingleOrDefault(e => e.Name == "SavingChanges");

With the EventInfo reference and the ObjectContext reference, we can now create a delegate and add a handler to the event:

C#
var handler = Delegate.CreateDelegate(saveChangesEvent.EventHandlerType, this, "OnSaveChanges");
saveChangesEvent.AddEventHandler(objectContext,handler);

If all you’re interested in doing is getting a SaveChanges event then you’re done. Your event handler will fire each time SaveChanges is called on your context! If you’re interested in seeing how to extract the T-SQL which is committed to the database on SaveChanges then keep reading.

Note: This section makes heavy use of undocumented guts of Entity Framework that are subject to change on new releases of EF. This code has been tested using .NET4.0 with EF5 (4.4)

Before I dive into the code, it’s best to get an overview of what’s happening when SaveChanges gets called. Here is a sequence diagram showing the typical flow (note A LOT of information is missing from this diagram). It only shows component usage that we’re interested in:

Image 2

The aim of the game here is to generate a collection of DbCommand objects (if you want more info, look at the source for DynamicUpdateCommand for an example) which will be executed sequentially in a loop. So we can see from the diagram that ObjectContext and EntityAdapter aren’t really doing very much it terms of command generation. Most of the work occurs in the UpdateTranslator and the implementation of UpdateCommand(abstract class) itself.

In order to retrieve the CommandTextfrom each command we need to replicate this functionality and gain access to the underlying DbCommand for each UpdateCommand (usually DynamicUpdateCommand as you’ll see if you debug the code). The best place to do this is within the callback for SaveChanges which we hooked earlier, as we can grab a new reference to ObjectContext directly by using "object sender" parameter.

The first target is to create an instance of UpdateTranslator. The ctor of this class has four parameters, ObjectStateManager, MetadataWorkspace, EntityConnection and Int? ConnectionTimeout. We can grab our parameters by using similar reflection to what was used before:

C#
var conn = sender.GetType()
                 .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                 .Where(p => p.Name == "Connection")
                 .Select(p => p.GetValue(sender,null))
                 .SingleOrDefault();
var entityConn = (EntityConnection) conn;
  
var objStateManager = (ObjectStateManager)sender.GetType()
      .GetProperty("ObjectStateManager", BindingFlags.Instance | BindingFlags.Public)
      .GetValue(sender,null);
var workspace = entityConn.GetMetadataWorkspace();

Then subsequently create our instance of UpdateTranslator:

C#
var translatorT = 
    sender.GetType().Assembly.GetType("System.Data.Mapping.Update.Internal.UpdateTranslator");
var translator = Activator.CreateInstance(translatorT,BindingFlags.Instance | 
    BindingFlags.NonPublic,null,new object[] {objStateManager,workspace,
    entityConn,entityConn.ConnectionTimeout },CultureInfo.InvariantCulture);

OK next up we need to call ProduceCommands on the UpdateTranslator. This returns a collection of UpdateCommands which we can enumerate:

C#
var produceCommands = translator.GetType().GetMethod(
   "ProduceCommands", BindingFlags.NonPublic | BindingFlags.Instance);
var commands = (IEnumerable<object>) produceCommands.Invoke(translator, null);

Then lastly we can enumerate the return and extract the DbCommand. You’ll probably know what to do with the DbCommand object so I won’t go any further with this explanation.

C#
var dcmd = (DbCommand)cmd.GetType()
                   .GetMethod("CreateCommand", BindingFlags.Instance | BindingFlags.NonPublic)
                   .Invoke(cmd, new[] {translator, identifierValues});

I hope you found this article useful. Please let me know in the comments section if you have any questions or suggestions!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Daisy Solutions Ltd
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to in Entity Framework 6 Pin
Abdelhamid_m20-Aug-15 3:27
Abdelhamid_m20-Aug-15 3:27 
SuggestionSimplification Pin
Ghiath Zarzar26-Dec-14 3:06
Ghiath Zarzar26-Dec-14 3:06 
SuggestionLet it work with all providers Pin
Ghiath Zarzar26-Dec-14 2:38
Ghiath Zarzar26-Dec-14 2:38 
GeneralVery Nice Pin
arvind mepani9-Jul-14 19:05
arvind mepani9-Jul-14 19:05 
GeneralMy vote of 5 Pin
SamNaseri3-Dec-12 12:40
SamNaseri3-Dec-12 12:40 
QuestionProfiling LINQ selects? Pin
Gnomad2-Dec-12 15:01
Gnomad2-Dec-12 15:01 
AnswerRe: Profiling LINQ selects? Pin
Tom Cook (UK)2-Dec-12 22:53
Tom Cook (UK)2-Dec-12 22:53 
GeneralRe: Profiling LINQ selects? Pin
Gnomad3-Dec-12 14:19
Gnomad3-Dec-12 14:19 
GeneralMy vote of 5 Pin
Panta8229-Nov-12 5:15
Panta8229-Nov-12 5:15 
QuestionA bit less reflection Pin
DuncanSmart28-Nov-12 0:06
DuncanSmart28-Nov-12 0:06 
AnswerRe: A bit less reflection Pin
Tom Cook (UK)28-Nov-12 1:10
Tom Cook (UK)28-Nov-12 1:10 
GeneralMy vote of 5 Pin
Morshed Anwar27-Nov-12 8:54
professionalMorshed Anwar27-Nov-12 8:54 
Excellent!!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.