Following the release of Visual Studio 2010 and .NET 4.0 Beta 2, the Entity Framework team put out a second CTP for Entity Framework 4.0, which includes enhancements to self-tracking entities. As the name implies, self-tracking entities know how to track their own state, so that it can be transmitted across service boundaries in an n-tier application. The idea is similar to what we’ve been able to do for several years with datasets. Datasets know how to keep track of their change-state and the table adapter knows how to inspect a dataset to generate insert, update and delete statements. However, because datasets are a .NET-specific type, it is not appropriate to expose them from services that should be platform-neutral. What we need is a service-oriented successor to the dataset that knows how to keep track of changes while remaining ignorant of how those changes will be persisted.
About a year ago I wrote an article for MSDN Magazine outlining one possible solution to this problem. Basically, I proposed attaching a property to each entity indicating its change-state (added, modified, deleted), and I supplied a custom collection capable of setting each entity’s change-state whenever it is added, modified or deleted. On the service-side, I wrote a helper class which interrogated entities for changes and persisted changes to the database, using either LINQ to SQL or the Entity Framework.
Entity Framework 4.0 uses the same basic approach, with entities tracking their own state and helper classes for change-tracking and persistence. The difference is that EF uses code-generation via T4 templates to create the helper classes instead of incorporating them into assemblies for the client and service to reference. I have to say that, although they could have built a client-side assembly without any dependencies on the Entity Framework, I have grown to like the code-gen approach. The reason is that T4 templates are totally open-source. If I don’t like what they did or want to change it in any way, all I have to do is create my own version of the template.
Lately I’ve had the chance to take STEs for a test drive, and I’ve put together a walk-through using the CTP 2 bits. You can download the sample application I created here. For convenience I’m using the Northwind sample database.
1. Open the Server Explorer in Visual Studio 2010 and a data connection to the Northwind database.
- You may need to attach Northwind to SQL Express using Management Studio.
- You may wish to add a RowVersion column of type timestamp to each table to facilitate concurrency management.
2. Create a class library project using Visual Studio 2010 and name it NorthwindData.
- Add a new item to the project, selecting "ADO.NET Entity Data Model," naming it Northwind.edmx.
- Select "Generate from Database" and choose the following tables:
Customers, Orders, Order_Details, Products, Categories
3. Right-click on the model design surface and select "Add Code Generation Item."
- Under the Code category select "ADO.NET Self-Tracking Entities" and name the T4 template Northwind.tt.
4. Add another class library project to the solution and name it NorthwindModel
- Move the Northwind.Types.tt template to the NorthwindModel project.
- Add a reference to System.Runtime.Serialization.
- Add a project reference for NorthwindModel in NorthwindData project.
- Open Northwind.Types.tt and modify inputFile initialization as follows:
string inputFile = @"..\NorthwindData\Northwind.edmx";
- Open Northwind.Context.tt and add "using NorthwindModel" directive in two places, both following <auto-generated> comment sections.
5. Add a WCF service to the solution (this can be a console app).
- Reference both NorthwindData and NorthwindModel projects.
- Add an interface called INorthwindService with a [ServiceContract] attribute and add methods with [OperationContract] attributes:
[ServiceContract]
public interface INorthwindService
{
[OperationContract]
Product GetProduct(int productId);
[OperationContract]
Order GetOrder(int orderId);
[OperationContract]
Order SaveOrder(Order order);
[OperationContract]
void DeleteOrder(Order order);
}
6. The GetOrder methods should use the Include operator to eager-load order details and products.
public Order GetOrder(int orderId)
{
using (NorthwindEntities ctx = new NorthwindEntities())
{
// Eager-load related entities
var query =
from o in ctx.Orders
.Include("OrderDetails.Product")
where o.OrderID == orderId
select o;
Order order = query.SingleOrDefault();
return order;
}
}
7. In the SaveOrder method call ApplyChanges on ctx.Orders, passing the incoming Order entity (inserts and updates).
- After calling ctx.SaveChanges, you will need to call AcceptChanges on both the order and each OrderDetail.
- It would be nice if calling AcceptChanges on an order would in turn call AcceptChanges on items in collection properties.
- The method should return the order that was saved in order to provide database-generated values (such as identity and concurrency fields).
- For convenience, it would be nice to have a HasChanges extension method for IObjectWithChangeTracker that checks to see if items in collection properties have been modified, added or removed.
public Order SaveOrder(Order order)
{
using (NorthwindEntities ctx = new NorthwindEntities())
{
// Inform object state mgr of changes
ctx.Orders.ApplyChanges(order);
// Persist changes to database
ctx.SaveChanges();
// Accept changes for order and details
order.AcceptChanges();
foreach (OrderDetail od in order.OrderDetails)
od.AcceptChanges();
return order;
}
}
8. In the DeleteOrder method mark order and order details as deleted, then call ApplyChanges.
- You will need to call ToList() on order.OrderDetails and then iterate the results to mark each item as deleted, so as not to modify the property during iteration. Then mark the order as deleted.
- It would be nice if marking an order as deleted would also mark order details as deleted.
public static void DeleteOrder(Order order)
{
using (NorthwindEntities ctx = new NorthwindEntities
(Settings.Default.NorthwindConnection))
{
// First mark details as deleted (from a list)
foreach (OrderDetail od in order.OrderDetails.ToList())
{
od.MarkAsDeleted();
}
// Then mark order as deleted
order.MarkAsDeleted();
// Inform object state mgr of changes
ctx.Orders.ApplyChanges(order);
// Persist changes to database
ctx.SaveChanges();
}
}
9. Add a client application to the project.
- First reference the NorthwindModel project, then add a service reference to the WCF service. This will ensure that classes generated from the STE T4 template will be used.
- Write code that creates a new order with two order details; change the order by adding a new detail, changing an existing detail, and removing a detail; then delete the entire order.
- It would be nice to have a GetChanges methods that obtains only items that have been added, modified or deleted, so that it is not necessary to send unchanged items to the service for updating.
10. In order to delete an order detail, it is not sufficient to simply remove it from the order. Rather, you will need to call MarkAsDeleted on the order detail.
- This behavior appears to be a bug, because removing an item from a collection should automatically mark it as deleted, just as adding or modifying an item automatically marks it as added or modified.
If you experimented with self-tracking entities in the first EF 4.0 CTP, based on Beta 1 of .NET 4.0 and Visual Studio 2010, you may have noticed a different method signature for ApplyChanges, in which you had to pass a delegate the returned an EntityChangeTrackerAdapter. While allowed you to supply your own adapter for converting change-state, the method signature for ApplyChanges is simpler and easier to use.
CTP 2 also improved the story of self-tracking entities by taking code for relationship fix-up out of the collection and placing it in the generated entity classes, where it takes advantage of the foreign key associations. The T4 templates for STEs also provide better support for concurrency management by preserving original values for properties that specify a ConcurrencyMode of Fixed in the entity data model. And the AcceptChanges method makes it easier to reinitialize an entity to an unmodified state after it’s been updated.
Hats off to the EF team for tackling n-tier scenarios head-on and coming up with an end-to-end solution for using the Entity Framework in service-oriented applications. Keep up the good work!