Click here to Skip to main content
15,885,103 members
Articles / Programming Languages / C#

Free your model from view-imposed restraints with Entity Framework Interceptors

Rate me:
Please Sign up or sign in to vote.
3.86/5 (4 votes)
2 May 2009Ms-PL11 min read 23.9K   123   15  
Implementing Business Logic, Logging, and Validation for the Entity Framework.

Introduction

Hi there, and many thanks for reading my explorations of the Entity Framework. I hope that some of the writings here will be useful to you.

Right now, I'm still having lots of fun learning, and lately, it's the Entity Framework that was a bit over 8 months ago with NET 3.5 SP1. Whilst exploring the possibilities of it, I came across some interesting concerns, and looked around a bit for people who had advice on it. As I couldn't find much, I would like to share my findings with you, and ask for your informed opinions.

I ended up writing some interceptors for the Entity Framework. It's still a bit rough around the edges, but it may be of use to you, or give you ideas on how to make things easier for you. If you like the ideas presented here or end up using them, I'll be happy to know about it!

Background

In this article, I aim to take a short look at how to implement business logic for the Entity Framework, attempt to devise a more versatile way of managing the logic behind the model, and try to manage separating aspects.

Business Logic in the Entity Framework

In the Entity Framework itself, you could implement the business logic in four places that I know of:

On<Property>Changing partial method of the Entity class

The event is called when the property changes. The partial method is generated by the code generator. All you need to do is start typing 'partial' and it shows up. The function is called when a property is set. http://msdn.microsoft.com/en-us/library/cc716747.aspx.

SavingChanges event of the object context

When changes are persisted to the database or store, the object context's SavingChanges event is called, where you would inspect the changed ObjectStateEntrys with GetObjectStateEntries() and log, validate, or do other processing. You could use this as a replacement to the LINQ to SQL partial methods, but you get them all at once in a big list. http://msdn.microsoft.com/en-us/library/cc716714.aspx.

Where are the LINQ to SQL partial methods?

Unlike LINQ to SQL, Entity Framework does *not* have a partial instance-method Insert<table>, Update<table>, or Delete<table> to handle deletion of single instances, it seems. http://weblogs.asp.net/scottgu/archive/2007/07/11/linq-to-sql-part-4-updating-our-database.aspx.

AssociationChanged event of an EntityReference

You could use the EntityReference to the other entity (collection) to register for association or relationship changes. The event is called when the references changes (property changes) E.g., if you have a property 'Client', you also have a property 'ClientReference', which can be monitored for changes by adding an event handler to the AssociationChanged event. http://msdn.microsoft.com/en-us/library/cc716754.aspx.

Relationship/association changes and SavingChanges event

The object context's SavingChanges event can retrieve the changes with GetObjectStateEntries(), which includes changes made to associations; the ObjectStateEntry's IsRelationship property should be 'true' in that case. This would allow you to change associations as a 'work in progress' and only intercept the final result.

Before-load initialization and handling the removal of state tracking

Entity objects can be intercepted before they are stored by the context, or removed from storage. This is not the same as being added or removed from the database, but means that the object is added or removed to the collection of objects being tracked for changes by the context. This is where you could, for example, provide a new GUID to every new object's key. The ObjectStateManager's ObjectStateManagerChanged event is the event to intercept for this purpose. http://msdn.microsoft.com/en-us/library/system.data.objects.objectstatemanager.objectstatemanagerchanged.aspx.

Additional business logic outside the model

There are two other places known to me that natively support inserting additional logic:

Recap

All of these can be combined, but that makes for business logic in six distinct logical places, and as such, for a complex model, it might create a hard to oversee combination of classes.

The property change partial methods are quite useful, banning obviously incorrect user input, for example. They are absolutely needed in any model. The ObjectContext's and ObjectStateManager's event could require a lot of processing and if/elses. I'd want a solution there to keep that clean and single-purposed. Dynamic Data and Data Services are technologies on top of the model, limiting the re-use if you wanted to just keep your logic but use it with another technology.

Need for an Interceptor Mechanism

So this is all working well, super even, and Entity Framework is a great technology with a very low entry barrier, allowing developers to get gradually more advanced in it as they start delving into the XML. But I still feel like I'm missing something. Ideally, I'd want to:

  • group related logic (aspects) in single-purpose classes, separating the logging and validation concerns from the context's and the entities' code as much as possible
  • avoid cluttering my context class with hundreds of ifs or cases
  • easily add or remove logic to my pipeline, both hardcoded and configured so I don't need to redeploy
  • free my model of view-imposed restraints and make it re-usable for many views (admin view, editor view, client view, web view, ...)
  • provide a granular level of control to what happens to my entities, and the same for logging
  • make it easier to keep having a parameterless constructor on my entities so generated websites and views (for example, Dynamic Data) can deal with it better, but still not be able to do things I don't want happening. This increases Dynamic Data's use as a model capabilities test when it fully supports inheritance (Dynamic Data vNext still has issues at the time of writing, with derived classes that add navigation properties).

I quickly wrote a few classes that take care of most of my needs; if you feel that the standard options for entity validation and logging are a bit cumbersome, I encourage you to try this project out and give me ideas on how to design it better.

Entity Framework Interceptors

Design

I designed the interceptors with the following additional things in mind:

  • Keeping a single purpose to a class was hard in an object hierarchy. You have the choice to build your own interceptor hierarchy, using inheritance. This is likely to look a *lot* like the inheritance tree in your model. You also have the option to have the engine ('dispatcher') run all validators on all assignable types. I'm still undecided on how I'd like it best, so I kept the option to choose.
  • I wanted to be able to choose if the runtime would try to intercept types I didn't specify as something it should take a look at, and check if I specified a base type or interface it knows instead, or only handle the types I specified with the interceptors I specified.
  • I wanted to be able to use attributes, attributes on attributes, configuration, just hand it the relevant interceptors in the constructor, and to be able to group interceptors into a logical name so I know what I'm adding.
  • I added the interceptors to the context. They intercept context events, so I thought it was logical to add them there. The context is the ultimate responsible for the environment the entity object lives in, so that makes sense to me there too. Moreover, I can imagine that in another context, the same entity behaves differently and may even be invalid.

How they Work

The ObjectContextInterceptorDispatcher handles the object context's SavingChanges event, and also the ObjectStateManager's ObjectStateManagerChanged events. From there on, every time either something is loaded into the store or persisted into the database, the relevant entity type gets intercepted by any interceptors that are specified (or compatible if you so set the settings for it). You can do validation, logging, assign IDs, or add required values that are not filled in during these intercepts. If you throw an exception during the intercept, the save should be aborted by the context, keeping your store in a consistent state.

Using the Code

Initialize an ObjectContextInterceptorDispatcher to a Context Type

The following code sample illustrates creating an entity class that can be affected by interception:

C#
public partial class Entities
{
    //this class will handle all the events
    //and move dispatch them to the configured
    //interceptors
    private ObjectContextInterceptorDispatcher _dispatcher;
    partial void OnContextCreated()
    {
        //override settings found in config
        _dispatcher = new ObjectContextInterceptorDispatcher(this, 
          new ObjectContextInterceptorDispatcherSettings()
          {
            InheritanceBasedOnEntityTypes = true,
            InterceptUnmappedTypes = true,
          });
    }
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            //the dispatcher is IDisposable so dispose of it
            //when the context is disposed of
            _dispatcher.Dispose();
            _dispatcher = null;
        }
        base.Dispose(disposing);
    }
}

From then on, any policies or interceptors, added either by attribute or app.config, will automatically be loaded, also for classes derived from your designed object context.

Make Interceptors

There are a few kinds of interceptors, following different interfaces. I could choose a common interface for better compile-time checking, but they do not have that much in common. There are five types of interceptors:

  • ones that intercept when an entity is saved (IEntitySaveInterceptor)
  • interceptors watching for association changes (IAssociationSaveInterceptor)
  • interceptors handling changes in tracking (IEntityTrackingInterceptor)
  • raw event handlers as interceptors (IContextSaveInterceptor and IContextTrackingInterceptor), allowing for interceptors to be called before or after the other three types; this helps, for example, to allow for some post-validation
Entity Saves

Inherit from the abstract class EntitySaveInterceptor<T> for strongly typed access, or type it yourself and use the IEntitySaveInterceptor interface, allowing one class to intercept many entity types.

C#
public class ClientSaveInterceptor : EntitySaveInterceptor<Client>
{
    public override void InterceptEntityInsert(Client entity)
    {
        //add logic here
    }
    public override void InterceptEntityDelete(Client entity)
    {
        //add logic here
    }
    public override void InterceptEntityUpdate(Client entity, 
           IExtendedDataRecord originalvalues, 
           IEnumerable<string> changedproperties)
    {
        //add logic here
    }
}
Association Saves

Derive from the abstract class AssociationSaveInterceptor<PKTYPE, FKTYPE> for strongly typed access. Alternatively, the IAssociationSaveInterceptor interface provides untyped access.

C#
public class FK_ProductRegistration_Client_NoUpdateRule : 
       AssociationSaveInterceptor<Client, ProductRegistration>
{
    public FK_ProductRegistration_Client_NoUpdateRule()
    {
        Console.WriteLine("Intercepting FK_ProductRegistration_Client");
    }
    public override void InterceptAssociationSaveInsert
          (ObjectContext context, AssociationType association, 
           Client pk, ProductRegistration fk)
    {
        //add logic here
    }
    public override void InterceptAssociationSaveRemove
          (ObjectContext context, AssociationType association, 
           Client pk, ProductRegistration fk)
    {
        //if the fk has a new reference to an association, it was updated
        if (fk.Client != null) 
            throw new UnauthorizedAccessException("Product registrations " + 
            "cannot change clients. Remove the registration and add a new one.");
    }
}
Entities Loaded to or Unloaded from Tracking

Implement the IEntityTrackingInterceptor interface:

C#
public class IdSetterInterceptor : IEntityTrackingInterceptor{
    public void InterceptAddToTracking
        (System.Data.Objects.ObjectContext context, object obj)
    {
        //tries to set a new Guid to every 'Id' property on objects it handles
        try
        {
            PropertyInfo pi = obj.GetType().GetProperty("Id");
            if (pi != null)
            {
                Guid guid = (Guid)pi.GetValue(obj, null);
                if (guid.Equals(Guid.Empty))
                {
                    guid = Guid.NewGuid();
                    pi.GetSetMethod().Invoke(obj, new object[1] { guid });
                }
            }
        } catch { }
    }
    public void InterceptRemoveFromTracking
        (System.Data.Objects.ObjectContext context, object obj)
    { ; }
}
Raw Event: Context Save

Implement the IContextSaveInterceptor interface to handle the raw events from the ObjectContext's SavingChanges.

C#
public class AlwaysBeforeContextSaveInterceptor 
     : IContextSaveInterceptor
{
    public InterceptTime InterceptTime
    {
        get
        {
            return InterceptTime.Before;
        }
        set
        {
            ;
        }
    }
    public void InterceptSave
        (System.Data.Objects.ObjectContext context)
    {
        //this is like adding the interceptor functions
        //to the object context event handler
        //add logic here
    }
}
Raw Event: Load from database/Unload from Tracking

Implement the IContextTrackingInterceptor interface to handle events from the ObjectStateManager that handles the tracking of object states.

C#
public class AlwaysBeforeContextTrackingInterceptor : IContextTrackingInterceptor{
    public InterceptTime InterceptTime
    {
        get
        {
            return InterceptTime.Before;
        }
        set
        {
            ;
        }
    }
    public void InterceptTracking(System.Data.Objects.ObjectContext context, 
           System.ComponentModel.CollectionChangeAction action, object obj)
    {
        //this is similar to handling the raw ObjectStateManager event
        //add logic here
    }
}

Add Attributes to the Context Type

You either add attributes or use app.config. Both can be combined, but duplicate interceptors won't be detected, so if you added the same logging interceptor twice, you would get two log entries.

InterceptEntitySaveAttribute

Adds an entity save interceptor:

C#
[InterceptEntitySave(
InterceptedType = typeof(Client),
InterceptorType = typeof(ClientInterceptor))]
InterceptAssociationSaveAttribute
C#
[InterceptAssociationSave(
EdmName = "DataModel.FK_ProductRegistration_Client",
InterceptorType = typeof(FK_ProductRegistration_Client_NoUpdateRule))]
InterceptEntityTrackingAttribute
C#
[InterceptEntityTracking(
InterceptedType = typeof(Person),
InterceptorType = typeof(IdSetterInterceptor))]
InterceptTrackingAttribute

Handles the raw ObjectStateManager event:

C#
[InterceptTracking(
InterceptTime = InterceptTime.Before,
InterceptorType = typeof(TrackingInterceptor))]
InterceptSaveAttribute

Handles the raw ObjectContext event:

C#
[InterceptSave(
InterceptTime = InterceptTime.Before,
InterceptorType = typeof(SaveInterceptor))]

Attributes can be placed on an ObjectContext-derived class and the dispatcher will load the relevant interceptors. They can also be placed onto other attributes, forming a policy (see below).

Add Entries in app.config

If many people would use it, I will make an XSD scheme for it.

Example section:

XML
<configSections>
  <section name="context-interception" 
    type="LibEntityIntercept.Config.InterceptorPoliciesSection, 
          LibEntityIntercept, Version=0.1.0.0, Culture=neutral, 
          PublicKeyToken=null" allowLocation="true" 
    allowDefinition="Everywhere" 
    allowExeDefinition="MachineToApplication" 
    restartOnExternalChanges="true" 
    requirePermission="true" />
 </configSections>
 <context-interception>
  <policies>
   <add policy-name="Example7Policy" policy-type="">
    <entity-save-interceptors>
     <add intercept="EntityValidationSample3.Person, 
                        EntityValidationSample3" 
         handler="EntityValidationSample3.Example7.Person_NoSpaceInLastNameRule, 
                  EntityValidationSample3" />
     <add intercept="EntityValidationSample3.Employee, 
                         EntityValidationSample3" 
      handler="EntityValidationSample3.Example7.Employee_WageBoundariesRule, 
               EntityValidationSample3" />
     <add intercept="EntityValidationSample3.KeyAccountManager, 
                         EntityValidationSample3" 
        handler="EntityValidationSample3.Example7.KeyAccountManager_NoDeleteRule,
                 EntityValidationSample3" />
    </entity-save-interceptors>
    <association-interceptors>
     <add association="DataModel.FK_ProductRegistration_Client" 
       handler="EntityValidationSample3.Example7.
                FK_ProductRegistration_Client_NoUpdateRule, 
                EntityValidationSample3"/>
    </association-interceptors>
   </add>
  </policies>
  <contexts>
   <add context-type="EntityValidationSample3.Example7.InterceptedEntities, 
                         EntityValidationSample3">
    <policies>
     <clear />
     <add policy-name="Example7Policy" />
    </policies>
   </add>
  </contexts>
  <settings intercept-unmapped="true" 
      build-inheritance="true" no-delete="false" 
      no-insert="false" no-update="false"/>
</context-interception>

There is also an Example.conf file located in the LibEntityIntercept project folder.

Initialize the Dispatcher with Interceptors in the Constructor

You can also hard-code some interceptor settings into the dispatcher by adding them to the constructor call.

C#
_dispatcher = new ObjectContextInterceptorDispatcher(this, 
new ObjectContextInterceptorDispatcherSettings()
{
    InheritanceBasedOnEntityTypes = true,
    InterceptUnmappedTypes = true,
}, new CodePolicy());

You can pass policies or interceptor settings to the constructor and they will be added. All of the above three methods will be combined. However, for the dispatcher settings, there can be only one, so there is a precedence: code overrides config overrides attributes.

Other Attributes: Policies (a.k.a. Groups of Interceptors), Access Control

ControlledEntityAccessAttribute

This attribute prevents certain types from being loaded into the ObjectStateManager.

C#
[ControlledEntityAccess(
ControlledEntityAccessInterceptor.ControlledEntityAccessMode.DenySpecified,
typeof(KeyAccountManager))]
InterceptorPolicyAttribute

You can use InterceptorPolicyAttribute and AttributedInterceptorPolicyAttribute to group interceptors together into a logical name. For example:

C#
[InterceptEntityTracking(
    InterceptedType = typeof(Person),
    InterceptorType = typeof(IdSetterInterceptor))]
public class CodePolicy : AttributedInterceptorPolicyAttribute
{
}

Notice how you can put attributes on the AttributedInterceptorPolicyAttribute class. Alternatively, you can do it all by code and implement the abstract class InterceptorPolicyAttribute.

Dispatcher Settings

InheritanceBasedOnEntityTypes

InterceptUnmappedTypes

Effect

false

false

Only specified interceptors will be used for specified types

true

false

Interceptors will intercept all assignable types if the types are intercepted themselves

false

true

Specified interceptors will intercept all types; if an exact match exists, that interceptor will be used; otherwise, the type is inspected and all interfaces and the base class will be used for interception

true

true

Interceptors will intercept all assignable types

Examples

I've included three sample projects with a total of 7 examples that can get you on your way experimenting with what I just wrote about. I've tried to illustrate how a combination of attributes, configuration, and constructor arguments can be used to combine interceptors for logging and validating changes. The solution comes with the little library I wrote, including source code for you to dabble in. Look at the program Main to get started. You may need to adapt the connection string to the database in each project, in app.config, before anything will run.

Caveats

Note on Associations

Both inheritance as well as property reference to another entity object are associations. So you will get association changes when inherited objects are made - you can intercept these changes too, but I'm not terribly sure what I'd use that for.

Note on Adding IDs Automatically

Keep in mind that if you forgot to specify PKs on your table and accidentally turned off your ID assignment, an error may occur during save and the changes will still be persisted! You will not be able to load the entities from the database again as the PK will yield two rows. Yes, I speak from experience. I learnt to always make sure the PK is unique at the database level too unless you want to make sure of it yourself, in code.

Nice to Have

  • A better, prettier configuration section (like WCF has)
  • Enforcement of DataAnnotation and ComponentModel namespace attributes like DynamicData does

Also Read

History

  • Released: 2009-05-02.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Technical Lead Kluwer Technology Services
Belgium Belgium
C# developer for a few years and specialized in an age past in C and Visual Basic 6. Having had the opportunity to work with .NET 3.5 since it was released, I've been using it with great success (big thanks to you LINQ team) to create functionality faster and better than was possible in 2.0, and am now looking into .NET 4.0. Currently I work at Kluwer Technology Services where I'm a technical lead, helping my fellows, architecting solutions and spreading the knowledge. I’m also an MCPD for .NET 2.0 and 3.5.

Comments and Discussions

 
-- There are no messages in this forum --