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:
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.
class Program
{
private static void Main(string[] args)
{
var context = new TestContext();
context.HookSaveChanges(FuncDelegate);
var tt = new TestTable() { Col2 = "Testing 123" };
context.TestTables.Add(tt);
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:
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:
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:
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:
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:
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:
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:
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 CommandText
from 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:
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
:
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:
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.
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!