共用方式為


Generating Business Logic “Hooks” for EF4 Entities

Once again a question in the EF MSDN Forum has prompted a blog post where I can give a more complete answer.  If I understand things correctly, the person asking the question wanted a simple way to add business logic hooks to their entities which would be called whenever they did SaveChanges.  In EF4 we made available the basic building blocks for this kind of thing, but unfortunately it’s not quite as easy and discoverable as it ought to be.

Since I’m stuck sitting at home with my knee elevated as I recover from minor knee surgery, I thought I’d take a little time today to create something which should make this much easier.  You can find the result here.  It’s a visual studio extension file (VSIX) which installs a new T4 item template called EntityHooks.  If you right click on the EF designer background and choose “Add Code Generation Item…”, you should see a new EF EntityHooks Code Generator in the list of templates.  If you choose that option, it will add a new TT file to your project that implements the hooks.

It’s important to realize that this complements whatever other code generation you have going on—it doesn’t replace it.  So if you have already added a code generation item, this will just add another one which generates partial classes that add functionality to your existing partial classes.  Unfortunately, if this is the first code gen artifact you have added to your project, the designer doesn’t realize it doesn’t generate the full entities so it turns off the default codegen, and you will either need to add another codegen artifact or in the properties for your EDMX file set the codegen strategy property from None back to default.

Once this is done you can write your own partial class for any of your entities and implement any or all of the OnAdded, OnModified and OnDeleted partial methods.  These methods will be called whenever an instance of that entity type is in the appropriate state at the time SaveChanges is called, and the call will happen before the framework does any other part of SaveChanges.  So, if your method throws an exception, no save will happen.  This also means that you can modify other entities if you want, or cancel an operation on the particular entity by calling AcceptChanges on its ObjectStateEntry (which is passed as an argument to the partial method).

Here’s a simple example I used to test out the extension.  It’s a hook which takes Customer entities marked for deletion and instead of deleting them just prepends “D:” to the front of the company name as a signal that they are deleted but you still want to keep them around for historical info or something like that:

 public partial class Customer
{
    partial void OnDeleted(System.Data.Objects.ObjectStateEntry entry)
    {
        entry.ChangeState(EntityState.Modified);
        CompanyName = "D:" + CompanyName;
    }
}

With this class in place, any attempt to delete a Customer will instead become just a modification.

The extension should work with whatever code generation strategy (default, self-tracking, poco, any custom strategy you create) as long as you generate partial classes both for your entities and your context.  You will notice, though, that it introduces a dependency on the EF since the partial methods receive an ObjectStateEntry argument.  if you want this kind of thing with a truly POCO experience, then you would need to create some other type to abstract away this dependency.  For this exercise I just took the shortcut of passing a state entry because it makes it easy to change the state of the entity or perform other actions in a general way.

The way I implemented this was to output an IEntityHooks interface which declares three methods OnAddedHook, OnModifiedHook and OnDeletedHook, plus a partial class for each entity which implements the interface by having those methods call the corresponding OnAdded, OnModified or OnDeleted partial method which the partial class also declares (can’t have a partial method implement an interface method because the compiler will completely optimize away the partial method if no one implements it), and finally a partial class for the context which overrides the SaveChanges virtual method.  The new SaveChanges method retrieves entries from the ObjectStateManager which represent entities in the various states, casts the entities to the interface and then executes the appropriate hook.

If you want more details, install the VSIX file linked above, add the hook template to one of your projects and have a look.  Pretty straight-forward really.  Let me know if you have any questions.

- Danny

Comments

  • Anonymous
    April 23, 2010
    Thanks Danny, I'm sure I will be using this. Do you think this is the best way of setting CreatedBy CreatedDateTime LastModifiedBy LastModifiedDateTime Or is there a better way?

  • Anonymous
    April 23, 2010
    For CreatedBy and LastModifiedBy this is probably the best option.  For CreatedDateTime and LastModifiedDateTime I would be tempted to use triggers in the database and mark the properties as storegenerated.  That way the times are always based on the database server's clock rather than whatever client the code is running on.

  • Danny
  • Anonymous
    April 29, 2010
    Wow! This is great!! I'm wondering if there's a way to modify the .tt file so that it only creates the generated classes if it does not yet already exist.  The problem I see is that if I add code to the generated files to handle my logic, and then add some additional tables, I need to re-run the tool.  But when I re-run the tool, it generates all of the files again, wiping out any changes.  Is this easy to do?

  • Anonymous
    April 29, 2010
    You shouldn't need to modify the generated code at all to add your logic.  First off, keep in mind that these are partial classes so you can put your own code in a different file and have it be part of the same class.  Secondly, the generated code declares partial methods but doesn't implement them.  So you just need to implement the partial methods in your own file, and the generated code will call it.   In the example I mention above with Customer.OnDeleted, in that project I actually had three separate files which all contribute to the Customer class.  One file was generated as part of the default codegen and had the main code for the customer object including its properties, etc.  Another file was generated using the EntityHooks code generator from this post, and it declares the partial methods and calls them, and the third file I wrote by hand to contain my business logic.  If I change the model, then the first two files will be regenerated, but my business logic won't be touched at all.

  • Danny
  • Anonymous
    May 09, 2010
    With regards to adding ModifiedBy, CreatedBy etc, whats the recommended way to get the currently logged in web user when the EF Model is in a separate project?

  • Anonymous
    May 10, 2010
    This all depends on how you have authentication set up, and it's really not related to the EF or to the question of how your projects are structured.  For many auth schemes, you can use System.Security.Principal.WindowsIdentity.GetCurrent().Name.ToString() to get the current identity for asp.net. You might take a look at a post like this: http://blogs.iis.net/sakyad/archive/2008/11/19/process-and-thread-identity-in-asp-net-a-practical-approach.aspx or something on MSDN for more info.

  • Danny
  • Anonymous
    October 09, 2010
    works great with the default entityobject generator.  However,  when i swtich to self tracking entities, it doesn't seem to work.   ObjectStateManager.GetObjectStateEntries does not return any items.  Is there a differenty approach that could be used with self tracking entities?

  • Anonymous
    October 09, 2010
    It really should work the same with self tracking entities as long as you reattach the entities / apply changes before you call SaveChanges.  The EF always uses the object state manager to figure out what changes it needs to send to the database, and these hooks use that same mechanism.  So if GetObjectStateEntries isn't returning any items, then saving won't have anything to work with either.  Hmmm...  I suppose the other possibility is that you might need to call DetectChanges first if you are using POCOs and the changes have been made while the entities were attached (not in self-tracking mode).

  • Danny
  • Anonymous
    November 12, 2010
    Nevermind on that last post, I just figured it out. File: Hooks.tt <code> // Emit Entity Types foreach (EntityType entity in ItemCollection.GetItems<EntityType>().Where(e => e.BaseType == null).OrderBy(e => e.Name)) { ... } </code> I added the  .Where(e => e.BaseType == null) to the emitter. Hope this can help someone else as well!

  • Anonymous
    April 11, 2011
    Hi Danny, I've installed the VSIX for EntitiyHooks but it doesn't show up in the list when try to code gen the model. Help! Thanks in advance.

  • Anonymous
    April 11, 2011
    @Srinivas, I'm not sure what's causing this difficulty.  Are you running VS2010?  Have you restarted visual studio?  Do you see other code gen items?  If so, which?

  • Danny
  • Anonymous
    November 08, 2011
    The comment has been removed