Self-Tracking entities: How to reduce exchange between client and server?

Self-Tracking entities are really great for N-Tiers scenarios but we have to be careful in order to reduce exchange between client and server.

Imagine the following EDM:

image

and the following scenario:

using (var context = new NorthwindClientContext())
{
    var c = context.Categories.AsQueryable().Include("Products.OrderDetails.Order.Employee").Include("Products.OrderDetails.Order.Customer.CustomerDemographics").Include("Products.OrderDetails.Order.Customer.member").First();
    var p = c.Products.First();
    var pName = p.ProductName;
    p.ProductName = "azerty";
    context.SaveChanges();
}

I define a SaveChanges method on the service:

ClientContext SaveChanges(ClientContext context);

[DataContract]
public class ClientContext
{
       [DataMember]
       public List<Customer> Customers { get; set; }
       [DataMember]
       public List<OrderDetail> OrderDetailSet { get; set; }
       [DataMember]
       public List<Order> Orders { get; set; }
       [DataMember]
       public List<Product> Products { get; set; }
       [DataMember]
       public List<CustomerDemographic> CustomerDemographics { get; set; }
       [DataMember]
       public List<Category> Categories { get; set; }
       [DataMember]
       public List<Employee> Employees { get; set; }
       [DataMember]
       public List<Member> Members { get; set; }
}

We need the ClientContext result because of identity and computed columns.

However, we probably don’t want to exchange all the entities with the server for only one property change.

My first idea was to call SaveChanges with this:

var clientContext = new ClientContext

       Customers = 
              (from e in Customers.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       OrderDetailSet = 
              (from e in OrderDetailSet.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       Orders = 
              (from e in Orders.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       Products = 
              (from e in Products.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       CustomerDemographics = 
              (from e in CustomerDemographics.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       Categories = 
              (from e in Categories.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       Employees = 
              (from e in Employees.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList(), 
       Members = 
              (from e in Members.AllEntities
               where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
               select e).ToList()
};

In fact this is not efficient because the only one modified entity is serialized with all its graph.

What I did in this case with my templates, is to exchange only one entity: Product { ProductID = 1, ProductName = “azerty” }. How do I do this?

First, I add a ModifiedProperties (List<string>) on ObjectChangeTracker class. Then, in my client SaveChanges method, I add the following:

var sentContext = new ClientContext();
sentContext.Customers = 
       (from e in clientContext.Customers
        select ReduceToModifications(e)).ToList();
sentContext.OrderDetailSet = 
       (from e in clientContext.OrderDetailSet
        select ReduceToModifications(e)).ToList();
sentContext.Orders = 
       (from e in clientContext.Orders
        select ReduceToModifications(e)).ToList();
sentContext.Products = 
       (from e in clientContext.Products
        select ReduceToModifications(e)).ToList();
sentContext.CustomerDemographics = 
       (from e in clientContext.CustomerDemographics
        select ReduceToModifications(e)).ToList();
sentContext.Categories = 
       (from e in clientContext.Categories
        select ReduceToModifications(e)).ToList();
sentContext.Employees = 
       (from e in clientContext.Employees
        select ReduceToModifications(e)).ToList();
sentContext.Members = 
       (from e in clientContext.Members 
        select ReduceToModifications(e)).ToList();

What is the ReduceToModifications method?

private Product ReduceToModifications(Product entity)
{
    Product value = new Product { ProductID = entity.ProductID };
    value.ChangeTracker.ChangeTrackingEnabled = true;
    value.ChangeTracker.State = entity.ChangeTracker.State;
    switch (entity.ChangeTracker.State)
    {
        case ObjectState.Added:
            value.ProductName = entity.ProductName;
            value.SupplierID = entity.SupplierID;
            value.CategoryID = entity.CategoryID;
            value.QuantityPerUnit = entity.QuantityPerUnit;
            value.UnitPrice = entity.UnitPrice;
            value.UnitsInStock = entity.UnitsInStock;
            value.UnitsOnOrder = entity.UnitsOnOrder;
            value.ReorderLevel = entity.ReorderLevel;
            value.Discontinued = entity.Discontinued;
            break;
        case ObjectState.Deleted:
            break;
        case ObjectState.Modified:
            value.ChangeTracker.ModifiedProperties = entity.ChangeTracker.ModifiedProperties;
            foreach (var modifiedPropery in entity.ChangeTracker.ModifiedProperties)
                // switch is more efficient than reflection
                switch (modifiedPropery)
                {
                    case "ProductName":
                        value.ProductName = entity.ProductName;
                        break;
                    case "SupplierID":
                        value.SupplierID = entity.SupplierID;
                        break;
                    case "CategoryID":
                        value.CategoryID = entity.CategoryID;
                        break;
                    case "QuantityPerUnit":
                        value.QuantityPerUnit = entity.QuantityPerUnit;
                        break;
                    case "UnitPrice":
                        value.UnitPrice = entity.UnitPrice;
                        break;
                    case "UnitsInStock":
                        value.UnitsInStock = entity.UnitsInStock;
                        break;
                    case "UnitsOnOrder":
                        value.UnitsOnOrder = entity.UnitsOnOrder;
                        break;
                    case "ReorderLevel":
                        value.ReorderLevel = entity.ReorderLevel;
                        break;
                    case "Discontinued":
                        value.Discontinued = entity.Discontinued;
                        break;
                    case "OrderDetails":
                        value.OrderDetails = entity.OrderDetails;
                        break;
 
                   case "Category":
                       value.Category = entity.Category;
                        break;
                }
            break;
        case ObjectState.Unchanged:
            break;
        default:
            throw new InvalidOperationException();
    }
    return value;
}

Then we have to deal with ObjectsAddedToCollectionProperties, ObjectsRemovedFromCollectionProperties and OriginalValues for relationships. Without this, we can’t report many to many relationships and we can’t be sure about the order of DB SQL commands.

So at the end my SaveChanges method is the following:

public void SaveChanges()
{
       var clientContext = new ClientContext 
      
              Customers = 
                     (from e in Customers.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              OrderDetailSet = 
                     (from e in OrderDetailSet.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              Orders = 
                     (from e in Orders.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              Products = 
                     (from e in Products.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              CustomerDemographics = 
                     (from e in CustomerDemographics.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              Categories = 
                     (from e in Categories.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              Employees = 
                     (from e in Employees.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList(), 
              Members = 
                     (from e in Members.AllEntities
                      where e.ChangeTracker.State != ObjectState.Unchanged || e.ChangeTracker.ObjectsAddedToCollectionProperties.Any() || e.ChangeTracker.ObjectsRemovedFromCollectionProperties.Any()
                      select e).ToList() 
       };

       var sentContext = new ClientContext();
       sentContext.Customers = 
              (from e in clientContext.Customers
               select ReduceToModifications(e)).ToList();
       sentContext.OrderDetailSet = 
              (from e in clientContext.OrderDetailSet
               select ReduceToModifications(e)).ToList();
       sentContext.Orders = 
              (from e in clientContext.Orders
               select ReduceToModifications(e)).ToList();
       sentContext.Products = 
              (from e in clientContext.Products
               select ReduceToModifications(e)).ToList();
       sentContext.CustomerDemographics = 
              (from e in clientContext.CustomerDemographics
               select ReduceToModifications(e)).ToList();
       sentContext.Categories = 
              (from e in clientContext.Categories
               select ReduceToModifications(e)).ToList();
       sentContext.Employees = 
              (from e in clientContext.Employees
               select ReduceToModifications(e)).ToList();
       sentContext.Members = 
              (from e in clientContext.Members
               select ReduceToModifications(e)).ToList();

       int nbCustomers = sentContext.Customers.Count;
       for (int index = 0 ; index < nbCustomers ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Customers[index], sentContext.Customers[index]);
       int nbOrderDetailSet = sentContext.OrderDetailSet.Count;
       for (int index = 0 ; index < nbOrderDetailSet ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.OrderDetailSet[index], sentContext.OrderDetailSet[index]);
       int nbOrders = sentContext.Orders.Count;
       for (int index = 0 ; index < nbOrders ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Orders[index], sentContext.Orders[index]);
       int nbProducts = sentContext.Products.Count;
       for (int index = 0 ; index < nbProducts ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Products[index], sentContext.Products[index]);
       int nbCustomerDemographics = sentContext.CustomerDemographics.Count;
       for (int index = 0 ; index < nbCustomerDemographics ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.CustomerDemographics[index], sentContext.CustomerDemographics[index]);
       int nbCategories = sentContext.Categories.Count;
       for (int index = 0 ; index < nbCategories ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Categories[index], sentContext.Categories[index]);
       int nbEmployees = sentContext.Employees.Count;
       for (int index = 0 ; index < nbEmployees ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Employees[index], sentContext.Employees[index]);
       int nbMembers = sentContext.Members.Count;
       for (int index = 0 ; index < nbMembers ; index ++)
              ReduceNavigationProperties(sentContext, clientContext.Members[index], sentContext.Members[index]); 

       Refresh(clientContext, base.Channel.SaveChanges(sentContext));
}

For Product, ReduceNavigationProperties is the following:

private void ReduceNavigationProperties(ClientContext context, Product originalValue, Product newValue)
{
    foreach (var relatedEntity in originalValue.ChangeTracker.OriginalValues)
    {
        switch (relatedEntity.Key)
        {
            case "Category":
                var categoryParentEntity = (Category)relatedEntity.Value;
                var newCategoryParentEntity = context.Categories.First(e => e.Id == categoryParentEntity.Id);
                newValue.ChangeTracker.OriginalValues.Add("Category", newCategoryParentEntity);
                ObjectList categoryParentEntityObjectList;
                if (!newCategoryParentEntity.ChangeTracker.ObjectsRemovedFromCollectionProperties.TryGetValue("Products", out categoryParentEntityObjectList))
                {
                    categoryParentEntityObjectList = new ObjectList();
                    newCategoryParentEntity.ChangeTracker.ObjectsRemovedFromCollectionProperties.Add("Products", categoryParentEntityObjectList);
                }
                categoryParentEntityObjectList.Add(newValue);
                newValue.CategoryID = originalValue.CategoryID;
                break;
        }
    }
    switch (originalValue.ChangeTracker.State)
    {

        case ObjectState.Added:
        case ObjectState.Deleted:
            foreach (var subEntity in originalValue.OrderDetails.Where(se => se.ChangeTracker.State != ObjectState.Unchanged))
            {
                var relatedEntity = context.OrderDetailSet.First(e => e.OrderID == subEntity.OrderID && e.ProductID == subEntity.ProductID);
                if (! newValue.OrderDetails.Contains(relatedEntity))
                    newValue.OrderDetails.Attach(relatedEntity);
            }
            if (originalValue.Category != null && originalValue.ChangeTracker.State == ObjectState.Unchanged)
            {
                var relatedEntity = context.Categories.First(e => e.Id == originalValue.Category.Id);
                if (newValue.Category != relatedEntity)
                    newValue.Category = relatedEntity;
            }
            break;
    }
}

With this code (generated with T4), the exchange between the client and the server is reduced to the minimum.

Now we have to use the ModifiedProperties for Update SQL Command (as described in my last post).

So instead of this:

context.ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);

We can use this:

context.ObjectStateManager.ChangeObjectState(entity, EntityState.Unchanged);
var ose = context.ObjectStateManager.GetObjectStateEntry(entity);
ose.SetModified();
foreach (var propertyName in entity.ChangeTracker.ModifiedProperties) 
    ose.SetModifiedProperty(propertyName);

In my sample case, the SQL command is the following:

exec sp_executesql N’update [dbo].[Products]
set [ProductName] = @0
where ([ProductID] = @1)
,N’@0 nvarchar(40),@1 int’,@0=N’azerty’,@1=1

The last point is the Refresh method. In it, we have to refresh identity property (for Add), computed properties for Add and Update, and identity FK when the relative entity is in the Added state. It is very important to do only these changes because we don‘t exchange the complete entity with the server but only a reduced clone.

This is my Refresh method:

private void Refresh(ClientContext clientContext, ClientContext dbContext)
{
    int customersCount = clientContext.Customers.Count;
    for (int i = 0 ; i < customersCount ; i ++)
    {
        var clientEntity = clientContext.Customers[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Customers.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Customers[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int orderDetailSetCount = clientContext.OrderDetailSet.Count;
    for (int i = 0 ; i < orderDetailSetCount ; i ++)
    {
        var clientEntity = clientContext.OrderDetailSet[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            OrderDetailSet.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.OrderDetailSet[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int ordersCount = clientContext.Orders.Count;
    for (int i = 0 ; i < ordersCount ; i ++)
    {
        var clientEntity = clientContext.Orders[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Orders.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Orders[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int productsCount = clientContext.Products.Count;
    for (int i = 0 ; i < productsCount ; i ++)
    {
        var clientEntity = clientContext.Products[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Products.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Products[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int customerDemographicsCount = clientContext.CustomerDemographics.Count;
    for (int i = 0 ; i < customerDemographicsCount ; i ++)
    {
        var clientEntity = clientContext.CustomerDemographics[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            CustomerDemographics.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.CustomerDemographics[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int categoriesCount = clientContext.Categories.Count;
    for (int i = 0 ; i < categoriesCount ; i ++)
    {
        var clientEntity = clientContext.Categories[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Categories.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Categories[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
    int employeesCount = clientContext.Employees.Count;
    for (int i = 0 ; i < employeesCount ; i ++)
    {
        var clientEntity = clientContext.Employees[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Employees.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Employees[i];
        bool typeFound = false;
        var clientEntityAsEmployeeInActivity = clientEntity as EmployeeInActivity;
        if (clientEntityAsEmployeeInActivity != null)
        {
            RefreshComputedValues(clientEntityAsEmployeeInActivity, (EmployeeInActivity)dbEntity);
            typeFound = true;
        }
        var clientEntityAsFiredEmployee = clientEntity as FiredEmployee;
        if (clientEntityAsFiredEmployee != null)
        {
            RefreshComputedValues(clientEntityAsFiredEmployee, (FiredEmployee)dbEntity);
            typeFound = true;
        }
        var clientEntityAsOutEmployee = clientEntity as OutEmployee;
        if (clientEntityAsOutEmployee != null)
        {
            RefreshComputedValues(clientEntityAsOutEmployee, (OutEmployee)dbEntity);
            typeFound = true;
        }
        if (! typeFound)
            RefreshComputedValues(clientEntity, dbEntity);
    }
    int membersCount = clientContext.Members.Count;
    for (int i = 0 ; i < membersCount ; i ++)
    {
        var clientEntity = clientContext.Members[i];
        if (clientEntity.ChangeTracker.State == ObjectState.Deleted)
        {
            Members.Detach(clientEntity);
            continue;
        }
        var dbEntity = dbContext.Members[i];
        RefreshComputedValues(clientEntity, dbEntity);
    }
}

For the RefreshComputedValues, I have the following for products:

private void RefreshComputedValues(Product entity, Product dbEntity)
{
    if (dbEntity.ChangeTracker.State == ObjectState.Added)
    {
        entity.ProductID = dbEntity.ProductID;
    }
    entity.IsDeserializing = true;
    if (dbEntity.Category != null && dbEntity.Category.ChangeTracker.State == ObjectState.Added)
        entity.CategoryID = dbEntity.CategoryID;
    entity.IsDeserializing = false;
    entity.ChangeTracker.AcceptChanges();
}

I know that I will repeat myself but what is really cool in my approach is the fact that all my code is generated. When my model is defined, I can generate the db and the biggest part of my code. Moreover my templates generate partial classes/interfaces and partial methods so I can extend my code, particularly to add Business Logic. Then, I just have to make the presentation layer.

Welcome to the data driven world! [:)]

This entry was posted in 10511, 7671, 7674, 7675. Bookmark the permalink.

5 Responses to Self-Tracking entities: How to reduce exchange between client and server?

  1. E.G. says:

    Great ideas! Would it be possible for you to post the code for this project?
    I’m trying to start implementing some of the ideas. Got the Update command to only generate SQL for the updated properties. While trying to implement the clien-to-server code, I came accross the “AllEntities” which is NOT referenced/found anywhere. What is this method?
    Once again, Thanks for your help.

  2. Martin Robins says:

    I am having a couple of problems with STEs on EF4 (although I cannot be sure that the problem is actually caused by STEs). I am trying to retrieve a graph of related objects (using Include) using a filter that applies to one of the child objects – the filter applies correctly but only the top level object is ever returned – never the child objects. More details at http://stackoverflow.com/questions/3595511/entity-framework-4-eager-loading-include-with-filters-using-self-tracking-enti but I wondered if you had any ideas?

  3. Matthieu MEZIL says:

    I just answered you on the forum.

  4. Frino says:

    Who do we write the check out to and it wont let me register. It keeps sainyg that Validation errors occurred. Please confirm the fields and submit it again with white boxes outlined in red covering the age and T-shirt places even though I have already filled them out.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>