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 ObjectStateEntry
s 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
if
s or case
s - 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:
public partial class Entities
{
private ObjectContextInterceptorDispatcher _dispatcher;
partial void OnContextCreated()
{
_dispatcher = new ObjectContextInterceptorDispatcher(this,
new ObjectContextInterceptorDispatcherSettings()
{
InheritanceBasedOnEntityTypes = true,
InterceptUnmappedTypes = true,
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_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.
public class ClientSaveInterceptor : EntitySaveInterceptor<Client>
{
public override void InterceptEntityInsert(Client entity)
{
}
public override void InterceptEntityDelete(Client entity)
{
}
public override void InterceptEntityUpdate(Client entity,
IExtendedDataRecord originalvalues,
IEnumerable<string> changedproperties)
{
}
}
Association Saves
Derive from the abstract class AssociationSaveInterceptor<PKTYPE, FKTYPE>
for strongly typed access. Alternatively, the IAssociationSaveInterceptor
interface provides untyped access.
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)
{
}
public override void InterceptAssociationSaveRemove
(ObjectContext context, AssociationType association,
Client pk, ProductRegistration fk)
{
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:
public class IdSetterInterceptor : IEntityTrackingInterceptor{
public void InterceptAddToTracking
(System.Data.Objects.ObjectContext context, object obj)
{
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
.
public class AlwaysBeforeContextSaveInterceptor
: IContextSaveInterceptor
{
public InterceptTime InterceptTime
{
get
{
return InterceptTime.Before;
}
set
{
;
}
}
public void InterceptSave
(System.Data.Objects.ObjectContext context)
{
}
}
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.
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)
{
}
}
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:
[InterceptEntitySave(
InterceptedType = typeof(Client),
InterceptorType = typeof(ClientInterceptor))]
InterceptAssociationSaveAttribute
[InterceptAssociationSave(
EdmName = "DataModel.FK_ProductRegistration_Client",
InterceptorType = typeof(FK_ProductRegistration_Client_NoUpdateRule))]
InterceptEntityTrackingAttribute
[InterceptEntityTracking(
InterceptedType = typeof(Person),
InterceptorType = typeof(IdSetterInterceptor))]
InterceptTrackingAttribute
Handles the raw ObjectStateManager
event:
[InterceptTracking(
InterceptTime = InterceptTime.Before,
InterceptorType = typeof(TrackingInterceptor))]
InterceptSaveAttribute
Handles the raw ObjectContext
event:
[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:
<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.
_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
.
[ControlledEntityAccess(
ControlledEntityAccessInterceptor.ControlledEntityAccessMode.DenySpecified,
typeof(KeyAccountManager))]
InterceptorPolicyAttribute
You can use InterceptorPolicyAttribute
and AttributedInterceptorPolicyAttribute
to group interceptors together into a logical name. For example:
[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