Download code for this article here.
There’s been some discussion lately in the blogosphere on
Self-Tracking Entities in EF4 and how well they might fit into a heterogeneous environment, where a Java client might consume a .NET service that exposes Self-Tracking Entities. While STE’s are placed in an assembly that does not reference the Entity Framework, the way in which change state is preserved in an STE is overly complex and seems to be designed to make it easier for EF to transmit changes to the ObjectStateManager on the service side. If you look closely at the ObjectChangeTracker class that is generated for the client, you’ll see that it maintains metadata for navigation properties with items that have been added or removed, as well as original values and extended properties. Requiring a non-.NET client to implement all that is asking an awful lot, and it couples the client too tightly to the service implementation.
About a year ago I wrote an
article for MSDN Magazine on how to track change-state on the client and transmit it to a service for persistence using LINQ to SQL, Entity Framework, or some other data access stack. Each entity has an ObjectState property indicating its change state (Unchanged, Added, Modified, Deleted). This piece of metadata is part of the data contract for each entity and is sent to the service so that inserts, updates and deletes can be performed against the database. The beauty of this approach is that it allows multiple changes to be sent to a service in a single round trip, where they can all be saved in a single transaction. The classic example of this is an Order with OrderDetails that have been added, modified or removed. An UpdateOrder method in the Data Access Layer can simply read the ObjectState property to insert, update or delete OrderDetails.
Fast forward to Entity Framework 4.0 and Visual Studio 2010. This week I implemented Trackable Data Transfer Objects with EF4 using the same basic architecture that I wrote about in the article. I use two sets of T4 templates (a code-generation technology built into Visual Studio). One set is used by the Data Access Layer on the service side to generate both the ObjectContext container class and POCO classes that serve as DTO’s but have an ObjectState property. On the client side there is an assembly containing another T4 template that generates DTO’s that also have an ObjectState property. DTO’s on the client also have a Tracking property (to turn change-tracking on and off) and navigation properties that are of type ChangeTrackingCollection<T>. This collection is placed in a separate ClientChangeTracker assembly that marks entities as Modified, Added or Deleted when a property changes or they are added or removed from the collection.
public enum ObjectState
{
Unchanged,
Added,
Modified,
Deleted
}
The goal of this design is to keep change state as minimal as possible: a simple ObjectState enum, which is exposed as a data contract by the service and can be easily implemented by a non-.NET client. While client-side DTO’s are generated by a T4 template that references the EF edmx file, there is nothing in the design of the application which requires this. In fact, the sample app I write for my article uses svcutil to generate client entities when adding a service reference in Visual Studio. The only reason why I went the T4 route is that I could more easily control the code generation process, and I very well may create a T4 template in the future that uses a service’s metadata (WSDL) to generate entities on the client.
On the service-side there is a ServiceChangeTracker assembly that has a TrackingHelper class with an ApplyChanges methods that extends ObjectContext by walking an object graph and informing the ObjectStateManager of entity state based on the ObjectState property.
public static void ApplyChanges<TEntity>(this ObjectContext context,
string entitySetName, TEntity entity) where TEntity : ITrackable
{
// First add and attach entities
context.AddAttachEntities(entitySetName, entity);
// Then delete entities
context.DeleteEntities(entitySetName, entity);
}
static void AddAttachEntities<TEntity>(this ObjectContext context,
string entitySetName, TEntity entity) where TEntity : ITrackable
{
// Iterate collection navigation properties
foreach (string navPropertyName in
context.GetNavigationProperties(entity))
{
// First, recursively add child entities
foreach (ITrackable navEntity in context.GetChildEntities
(entity, navPropertyName, ObjectState.Added))
{
context.AddAttachEntities(navPropertyName, navEntity);
}
// Recursively attach child modified and deleted entities
foreach (ITrackable navEntity in context.GetChildEntities
(entity, navPropertyName, ObjectState.Modified,
ObjectState.Deleted))
{
context.AddAttachEntities(navPropertyName, navEntity);
}
}
// Add or attach entity
switch (entity.ObjectState)
{
case ObjectState.Unchanged:
context.AttachTo(entitySetName, entity);
break;
case ObjectState.Added:
context.AddObject(entitySetName, entity);
break;
case ObjectState.Deleted:
context.AttachTo(entitySetName, entity);
break;
case ObjectState.Modified:
context.AttachTo(entitySetName, entity);
context.SetPropertiesAsModified(entitySetName, entity);
break;
default:
break;
}
}
static void DeleteEntities<TEntity>(this ObjectContext context,
string entitySetName, TEntity entity) where TEntity : ITrackable
{
// Iterate collection navigation properties
foreach (string navPropertyName in
context.GetNavigationProperties(entity))
{
// Recursively delete child entities
foreach (ITrackable navEntity in context.GetChildEntities
(entity, navPropertyName, ObjectState.Deleted).ToList())
{
context.DeleteEntities(navPropertyName, navEntity);
}
}
// Delete entity
if (entity.ObjectState == ObjectState.Deleted)
{
context.DeleteObject(entity);
}
}
When a DAL method invokes SaveChanges on the ObjectContext, inserts, updates and deletes are persisted to the database in the scope of a single transaction. The DAL can then call AcceptChanges in TrackingHelper to restore objects to an Unchanged state and return the updated object to the client with database-calculated values, such as identity and concurrency fields.
public Order SaveOrder(Order order)
{
using (NorthwindEntities ctx = new NorthwindEntities
(Settings.Default.NorthwindConnection))
{
// Apply entity changes
ctx.Orders.ApplyChanges(order);
// Save updated order
ctx.SaveChanges();
// Return entity to unchanged state
ctx.AcceptChanges(order);
// Return updated order
return order;
}
}
Trackable DTO’s match entities defined in the conceptual model, which allows us to leverage POCO support in EF4 to avoid creating two sets of classes and manually copying data between them. As such they represent an approach that combines the simplicity of self-tracking entities with the flexibility of DTO’s, without the extra baggage carried by STE’s. The use of two separate sets of T4 templates allows us to decouple client DTO’s from the service DTO’s, applying the rules of
data contract versioning so that they can diverge from one another in a robust fashion.
Anthony Sneed works as an instructor for DevelopMentor and is author of the course Essential LINQ with Entity Framework 4.0. You can reach him at
tonys@develop.com.