views:

271

answers:

2

Does anyone know of a way to do something similar to Django's signals using LINQ to SQL?

I'm trying to record when new rows are inserted and when certain columns are updated, so I really just want pre_save and post_save signals.

I can kind of do it with some models by using the partials defined like OnFooIDChanging() and OnFooIDChanged() (where FooID is a primary key), but this doesn't work for models whose primary key is not an identity, or is set by code.

For those, I could possibly use OnValidate(), but that would only be pre_save, and it makes dealing with the database tough, since OnValidate() is called from DBContext.SubmitChanges(), which of course doesn't allow a second SubmitChanges() to be called from within, making post_save basically impossible as far as I can see.

+1  A: 

Ok, I've gone completely down the rabbit hole on this one, but I think I have a pretty cool solution:

First, add an event handler to your data context that will collect all of the post-save signals and hide the Dispose method so that we can call the event right before we dispose. (Note that I use the new keyword instead of override. This makes calling the event possible.)

partial class MyDataContext
{
    internal delegate void PostSaveHandler();
    internal event PostSaveHandler PostSave;

    // This method hides the underlying Dispose because we need to call PostSave.
    public new void Dispose(bool disposing)
    {
        // Obviously necessary error handling omitted for brevity's sake
        PostSave();
        base.Dispose(disposing);
    }
}

Next, write a T4 Template that inspects the dbml file that Linq to Sql generates for you.

<#
var dbml = XDocument.Load(@"MyDataContext.dbml");
var name = XName.Get("Type", "http://schemas.microsoft.com/linqtosql/dbml/2007");
var tables = from t in dbml.Descendants(name) select t.Attribute("Name").Value;
foreach(var table in tables)
{
#>
    ...

For each table in the database (and thus each partial class), add on to the partial with the following methods.

public partial class Foo
{
    internal void OnInsert(MyDataContext db) {
        PreInsert();
        db.PostSave += delegate { PostInsert(); };
    }
    internal void OnUpdate(MyDataContext db) {
        PreUpdate();
        db.PostSave += delegate { PostUpdate(); };
    }
    internal void OnDelete(MyDataContext db) {
        PreDelete();
        db.PostSave += delegate { PostDelete(); };
    }
    partial void PreInsert();
    partial void PostInsert();
    partial void PreUpdate();
    partial void PostUpdate();
    partial void PreDelete();
    partial void PostDelete();
}

// repeat for all tables

Also add another partial MyDataContext via T4. This will be adding definitions to the partial methods that Linq to SQL gives you (as Merritt mentioned).

public partial class MyDataContext
{
    // Add these three partial methods for each table
    partial void InsertFoo(Foo foo)
    {
        foo.OnInsert(this);
        ExecuteDynamicInsert(foo);
    }
    partial void UpdateFoo(Foo foo)
    {
        foo.OnUpdate(this);
        ExecuteDynamicUpdate(foo);
    }
    partial void DeleteFoo(Foo foo)
    {
        foo.OnDelete(this);
        ExecuteDynamicDelete(foo);
    }

    // ...
}

Hide those files away somewhere safe, so no one tries to mess with them.

Your signals framework is set up. Now you can write your signals. Put these either in Foo.cs or all together in a Signals.cs file:

partial class Foo
{
    partial void PostInsert()
    {
        EventLog.AddEvent(EventType.FooInserted, this);
    }
}

This is a bit complex, so if anything doesn't make sense, please leave a comment and I'll do my best to address it.

tghw
+1  A: 

I have a much easier solution than what I already posted which didn't work anyway: override SubmitChanges(ConflictMode failureMode):

partial class MyDataContext
{
     // SubmitChanges() calls this method after inserting default value for param
     public override void SubmitChanges(ConflictMode failureMode)
     {

          // Pre-Submit Changes

          //Updates            
          for (int changeCounter = 0; changeCounter < this.GetChangeSet().Updates.Count; changeCounter++)
          {                
               var modifiedEntity = this.GetChangeSet().Updates[changeCounter];                
               // Do something, for example:
               // var tableXEntry = new TableX() { Prop1 = "foo" };
               // this.tableXEntries.InsertOnSubmit(tableXEntry );
          }            

          //Inserts            
          for (int changeCounter = 0; changeCounter < this.GetChangeSet().Inserts.Count; changeCounter++)            
          {                
               object modifiedEntity = this.GetChangeSet().Inserts[changeCounter];                
               // Do Something
          }


          // Submit Changes
          base.SubmitChanges(failureMode);


          // Post Submit Changes

          //Updates            
          for (int changeCounter = 0; changeCounter < this.GetChangeSet().Updates.Count; changeCounter++)
          {                
               var modifiedEntity = this.GetChangeSet().Updates[changeCounter];                
               // Do something, for example:
               // var tableXEntry = new TableX() { Prop1 = "foo" };
               // this.tableXEntries.InsertOnSubmit(tableXEntry );
          }            

          //Inserts            
          for (int changeCounter = 0; changeCounter < this.GetChangeSet().Inserts.Count; changeCounter++)            
          {                
               object modifiedEntity = this.GetChangeSet().Inserts[changeCounter];                
               // Do Something
          }
}

With the Entity Framework, I do something similar to what you are trying to do: after I save a entity, I insert a new entry into a different table for auditing purposes (it's a copy of the entity before the changes). there is a SaveChanges() event on the EF entities container (like the data context) that allows you to add to the current context before changes are saved.

Merritt