EF: Another (better ?) way to deal with POCO entities

If you want to have POCO entities and to keep at the same time, the context features like tracking, attaching automatically relationships, Lazy loadings, EF will generate a proxy which inherits your entity classes and which adds some code to do it. This is the same idea than in N-Hibernate. However, in my opinion, it is a bad idea. Indeed, it is very restrictive approach because you can’t have your entities sealed and need to have them public, you must have gets and sets protected or public and virtual, idem for constructor: you have to have a public or protected parameterless constructor. For these reasons, I really don’t like this approach.

I think I am not the only guy who doesn’t like it. However, what else can we do?

My idea was to use Cecil to update my entities instead of inheriting them.

How does it work?

I define a program with two arguments in the Main method: the “normal” entity assembly path and the edmx path. In this program, I inject IL in the existing entities so that they implement INotifyPropertyChanging, INotifyPropertyChanged, IEntityWithChangeTracker, IEntityWithKey and IEntityWithRelationships.

Then in my application, I use the following post-build event:

"D:\Documents\Visual Studio 2010\Projects\EF POCO with Cecil\EFPOCOWithCecil\bin\Release\EFPOCOWithCecil.exe" "$(TargetDir)Entities.dll" "$(ProjectDir)..\DAL\Northwind.edmx"

With these implementations, I can enjoy context features.

I have only one restriction: I have to set navigation properties collection as ICollection<T> (it’s the same with EF to use POCO proxy).

The good point is the fact that we don’t have other restrictions of default POCO implementation. Moreover with EF, if you use CreateObject on your context, you will have an instance of the proxy and if you use directly a new on your entity type, you won’t have the proxy (so without context features). This point is very confusing. With my approach, this problem is solve too.

You can download my code here.

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! Smile

EF and N-Tiers

Even if it’s really easier with Self-Tracking entities and EF4, a difficult point remains in N-Tiers scenarios.

In a previous post, I explained how with T4, I can generate all my WCF service. Now, what about the client tiers?

I explained how can I use LINQ queries and translate them into WCF service methods.

Now my objective is to have a real context on the client tier. I already did it for ADO .NET Data Services using T4, I now have one (still with T4 of course) for WCF.

One of my customers told me that he didn’t like the fact that with ADO .NET Data Services, when you want to access to the context collection, it implies a REST query by default. He wanted to use the cache by default.

With my context, I have this and I use AsQueryable extension method to generate a query on the server.

[TestClass]
public class UnitTest
{
    [TestMethod]
    public void TestService()
    {
        INorthwindService context = new NorthwindClientContext();
        Assert.AreNotEqual(0, context.GetCustomers().Count);
   

    [TestMethod]
    public void TestFilter()
    {
        INorthwindService context = new NorthwindClientContext();
        Assert.AreEqual(1, context.GetCustomers(null, "it.CustomerID='ALFKI'", null, null, null).Count);
   

    [TestMethod]
    public void TestLINQWhere()
    {
        using (var context = new NorthwindClientContext())
        {
            Assert.AreEqual(1, (from c in context.Customers.AsQueryable()
                                where c.CustomerID == "ALFKI"
                                select c).Count());
        }
   

    [TestMethod]
    public void TestLINQWhere2()
    {
        using (var context = new NorthwindClientContext())
        {
            var customerID = "ALFKI";
            Assert.AreEqual(1, (from c in context.Customers.AsQueryable()
                                where c.CustomerID == customerID
                                select c).Count());
        }
   

    [TestMethod]
    public void TestLINQWhere3()
    {
        using (var context = new NorthwindClientContext())
        {
            var customer = new Customer { CustomerID = "ALFKI" };
            Assert.AreEqual(1, (from c in context.Customers.AsQueryable()
                                where c.CustomerID == customer.CustomerID
                                select c).Count());
        }
    }  

    [TestMethod]
    public void TestLINQWhere4()
    {
        using (var context = new NorthwindClientContext())
        {
            Assert.AreNotEqual(0, (from o in context.Orders.AsQueryable()
                                   where o.OrderDate > new DateTime(1997, 1, 31)
                                   select o).Count());
        }
    }

   
[TestMethod]
    public void TestLINQWhere5()
    {
        using (var context = new NorthwindClientContext())
        {
            var d = new DateTime(1997, 1, 31);
            Assert.AreNotEqual(0, (from o in context.Orders.AsQueryable()
                                   where o.OrderDate > d
                                   select o).Count());
        }
    }  

    [TestMethod]
    public void TestLINQTake()
    {
        using (var context = new NorthwindClientContext())
        {
            Assert.AreEqual(2, (from c in context.Customers.AsQueryable()
                                select c).Take(2).Count());
        }
   

    [TestMethod]
    public void TestFirstAndInclude()
    {
        using (var context = new NorthwindClientContext())
        {
            var order = (from o in context.Orders.AsQueryable().Include(Order.CUSTOMER_NAME).Include(Order.ORDERDETAILS_NAME)
                         where o.ShipCity == "PARIS"
                         orderby o.OrderDate
                         select o).Skip(2).First();
            Assert.IsNotNull(order.Customer);
            Assert.AreEqual(1, context.Customers.Count);
            Assert.AreEqual(order.Customer, context.Customers[0]);
            Assert.AreEqual(1, context.Orders.Count);
            Assert.AreEqual(order, context.Orders[0]);
            Assert.AreNotEqual(0, order.OrderDetails);
        }
   

    [TestMethod]
    public void NoTracking()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.NoTracking;
            var l = context.Customers.AsQueryable().ToList();
            Assert.AreEqual(0, context.Customers.Count);
            Assert.AreNotEqual(0, l.Count);
        }
   

    [TestMethod]
    public void AppendOnly()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.AppendOnly;
            var l = context.Customers.AsQueryable().ToList();
            Assert.AreEqual(l.Count, context.Customers.Count);
        }
   

    [TestMethod]
    public void AppendOnly2()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.AppendOnly;
            var c = new Customer { CustomerID = "ALFKI" };
            context.Customers.Attach(c);
            context.Customers.AsQueryable().ToList();
            Assert.IsNull(c.CompanyName);
            Assert.AreEqual(0, c.ChangeTracker.ModifiedProperties.Count);
        }
   

    [TestMethod]
    public void OverwriteChanges()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.OverwriteChanges;
            var l = context.Customers.AsQueryable().ToList();
            Assert.AreEqual(l.Count, context.Customers.Count);
        }
   

    [TestMethod]
    public void OverwriteChanges2()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.OverwriteChanges;
            var c = new Customer { CustomerID = "ALFKI" };
            context.Customers.Attach(c);
            context.Customers.AsQueryable().ToList();
            Assert.IsNotNull(c.CompanyName);
            Assert.AreEqual(0, c.ChangeTracker.ModifiedProperties.Count);
        }
    }

    [TestMethod]
    public void PreserveChanges()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.PreserveChanges;
            var l = context.Customers.AsQueryable().ToList();
            Assert.AreEqual(l.Count, context.Customers.Count);
        }
   

    [TestMethod]
    public void PreserveChanges2()
    {
        using (var context = new NorthwindClientContext())
        {
            context.MergeOption = MergeOption.PreserveChanges;
            var c = new Customer { CustomerID = "ALFKI" };
            context.Customers.Attach(c);
            context.Customers.AsQueryable().ToList();
            Assert.IsNull(c.CompanyName);
            Assert.AreNotEqual(0, c.ChangeTracker.ModifiedProperties.Count);
            Assert.IsTrue(c.ChangeTracker.ModifiedProperties.Contains("CompanyName"));
        }
   

    [TestMethod]
    public void Save()
    {
        using (var context = new NorthwindClientContext())
        {
            //Add
            var psCount = context.Products.AsQueryable().Count();
            var p = new Product { ProductName = "p" };
            context.Products.Add(p);
            context.SaveChanges();
            Assert.AreNotEqual(0, p.ProductID);
            Assert.AreEqual(ObjectState.Unchanged, p.ChangeTracker.State);
            Assert.AreEqual(psCount + 1, context.Products.AsQueryable().ToList().Count); 

            //Update
            p.ProductName = "p2";
            Assert.AreEqual(1, p.ChangeTracker.ModifiedProperties.Count);
            Assert.AreEqual("ProductName", p.ChangeTracker.ModifiedProperties[0]);
            context.SaveChanges();
            context.MergeOption = MergeOption.NoTracking;
            var p2 = context.Products.AsQueryable().Where(pr => pr.ProductID == p.ProductID).First();
            Assert.AreNotEqual(p, p2);
            Assert.AreEqual("p2", p2.ProductName); 

            //Remove
            context.Products.Remove(p);
            Assert.AreEqual(ObjectState.Deleted, p.ChangeTracker.State);
            context.SaveChanges();
            Assert.AreEqual(psCount, context.Products.AsQueryable().ToList().Count);
        }
   

    [TestMethod]
    public void Save2()
    {
        using (var context = new NorthwindClientContext())
        {
            var csCount = context.Categories.AsQueryable().Count();
            var c1 = new Category { CategoryName = "cn" };
            context.Categories.Add(c1);
            var psCount = context.Products.AsQueryable().Count();
            var p1 = new Product { ProductName = "p1" };
            var p2 = new Product { ProductName = "p2" };
            context.Products.Add(p1);
            context.Products.Add(p2);
            context.SaveChanges();
            using (var context2 = new NorthwindClientContext())
            {
                Assert.AreEqual(csCount + 1, context2.Categories.AsQueryable().Count());
                Assert.AreEqual(psCount + 2, context2.Products.AsQueryable().Count());
            }
            context.Categories.Remove(c1);
            context.Products.Remove(p1);
            p2.ProductName = "p2Bis";
            context.SaveChanges();
            using (var context2 = new NorthwindClientContext())
            {
                Assert.AreEqual(csCount, context2.Categories.AsQueryable().Count());
                Assert.AreEqual(psCount + 1, context2.Products.AsQueryable().Count());
                Assert.AreEqual("p2Bis", context2.Products.Last().ProductName);
            }
            context.Products.Remove(p2);
            context.SaveChanges();
            using (var context2 = new NorthwindClientContext())
            {
                Assert.AreEqual(psCount, context2.Products.AsQueryable().Count());
            }
        }
   

    [TestMethod]
    public void TestSaveManyToMany()
    {
        CustomerDemographic cd;
        using (var context = new NorthwindClientContext())
        {
            cd = new CustomerDemographic { CustomerTypeID = "CD", CustomerDesc = "cd desc" };
            context.CustomerDemographics.Add(cd);
            context.SaveChanges();
        }
        using (var context = new NorthwindClientContext())
        {
            var c = context.Customers.AsQueryable().First();
            c.CustomerDemographics.Add(cd);
            Assert.AreEqual(ObjectState.Unchanged, cd.ChangeTracker.State);
            Assert.AreEqual(1, context.CustomerDemographics.Count);
            Assert.AreEqual(1, cd.Customers.Count);
            context.SaveChanges();
        }
        using (var context = new NorthwindClientContext())
        {
            var c = context.Customers.AsQueryable().Include(Customer.CUSTOMERDEMOGRAPHICS_NAME).First();
            Assert.AreNotEqual(0, context.CustomerDemographics.Count);
            Assert.AreNotEqual(0, cd.Customers.Count);
            context.CustomerDemographics.Remove(context.CustomerDemographics.First(cd2 => cd2.CustomerTypeID.TrimEnd() == "CD"));
            context.SaveChanges();
        }
   

    [TestMethod]
    public void TestAddViaCollection()
    {
        using (var context = new NorthwindClientContext())
        {
            int psCount;
            using (var context2 = new NorthwindClientContext())
            {
                psCount = context2.Products.AsQueryable().Count();
            }
            var c = context.Categories.AsQueryable().First();
            var p = new Product { ProductName = "pn" };
            c.Products.Add(p);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(1, context.Products.Count);
            Assert.AreEqual(context.Products[0], p);
            Assert.AreEqual(p.Category, c);
            Assert.AreEqual(p.CategoryID, c.Id);
            Assert.AreEqual(ObjectState.Added, p.ChangeTracker.State);
            context.SaveChanges();
            using (var context2 = new NorthwindClientContext())
            {
                Assert.AreEqual(psCount + 1, context2.Products.AsQueryable().Count());
            }
            Assert.AreEqual(1, c.Products.Count);
            context.Products.Remove(p);
            Assert.AreEqual(0, c.Products.Count);
            context.SaveChanges();
        }
   

    [TestMethod]
    public void TestAddViaCollection2()
    {
        using (var context = new NorthwindClientContext())
        {
            int psCount;
            using (var context2 = new NorthwindClientContext())
            {
                psCount = context2.Products.AsQueryable().Count();
            }
            var c = context.Categories.AsQueryable().First();
            var p = new Product { ProductName = "pn", Category = c };
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(1, context.Products.Count);
            Assert.AreEqual(context.Products[0], p);
            Assert.AreEqual(p.Category, c);
            Assert.AreEqual(p.CategoryID, c.Id);
            Assert.AreEqual(ObjectState.Added, p.ChangeTracker.State);
            context.SaveChanges();
            using (var context2 = new NorthwindClientContext())
            {
                Assert.AreEqual(psCount + 1, context2.Products.AsQueryable().Count());
            }
            Assert.AreEqual(1, c.Products.Count);
            context.Products.Remove(p);
            Assert.AreEqual(0, c.Products.Count);
            context.SaveChanges();
        }
   

    [TestMethod]
    public void TestAddViaCollection3()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { CategoryName = "cn" };
            context.Categories.Add(c);
            var p = new Product { ProductName = "pn", Category = c };
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(1, context.Products.Count);
            Assert.AreEqual(context.Products[0], p);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(c.Id, p.CategoryID);
            Assert.AreEqual(1, context.Categories.Count);
            Assert.AreEqual(c, context.Categories[0]);
            context.SaveChanges();
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(c.Id, p.CategoryID);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
            context.Categories.Remove(c);
            Assert.IsNull(p.Category);
            Assert.IsNull(p.CategoryID);
            Assert.AreEqual(ObjectState.Modified, p.ChangeTracker.State);
            Assert.AreEqual(1, p.ChangeTracker.ModifiedProperties.Count);
            Assert.AreEqual("CategoryID", p.ChangeTracker.ModifiedProperties[0]);
            context.SaveChanges();
            context.Products.Remove(p);
            context.SaveChanges();
        }
   

    [TestMethod
    public void TestAddViaCollection4()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { CategoryName = "cn" };
            var p = new Product { ProductName = "pn", Category = c };
            context.Categories.Add(c);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(1, context.Products.Count);
            Assert.AreEqual(context.Products[0], p);
            Assert.AreEqual(p.Category, c);
            Assert.AreEqual(p.CategoryID, c.Id);
            Assert.AreEqual(1, context.Categories.Count);
            Assert.AreEqual(c, context.Categories[0]);
        }
   

    [TestMethod]
    public void TestAddViaCollection5()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Customer { CustomerID = "A" };
            var cd = new CustomerDemographic();
            c.CustomerDemographics.Add(cd);
            context.Customers.Attach(c);
            Assert.AreEqual(1, context.Customers.Count);
            Assert.AreEqual(1, context.CustomerDemographics.Count);
            Assert.AreEqual(c, context.Customers[0]);
            Assert.AreEqual(cd, context.CustomerDemographics[0]);
            Assert.AreEqual(1, cd.Customers.Count);
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void TestEntitySetAttachWithSameKey()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Categories.Attach(new Category { Id = 1 });
            context.Categories.Attach(new Category { Id = 1 });
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void TestEntitySetAddWithSameKey()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Customers.Add(new Customer { CustomerID = "C" });
            context.Customers.Add(new Customer { CustomerID = "C" });
        }
   

    [TestMethod]
    public void TestEntitySetAddWithSameKeyIdentity()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Categories.Add(new Category { Id = 0 });
            context.Categories.Add(new Category { Id = 0 });
        }
   

 
   [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void TestNavigationPropertyManyAttachWithSameKey()
    {
        using (var context = new NorthwindClientContext())
        {
            var cd = new CustomerDemographic { CustomerTypeID = "CD" };
            context.CustomerDemographics.Attach(cd);
            context.Customers.Attach(new Customer { CustomerID = "C" });
            cd.Customers.Attach(new Customer { CustomerID = "C" });
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void TestNavigationPropertyManyAddWithSameKey()
    {
        using (var context = new NorthwindClientContext())
        {
            var cd = new CustomerDemographic { CustomerTypeID = "CD" };
            context.CustomerDemographics.Attach(cd);
            context.Customers.Attach(new Customer { CustomerID = "C" });
            cd.Customers.Add(new Customer { CustomerID = "C" });
        }
   

    [TestMethod]
    public void MultipleAddAttachOfSameObject()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Customer { CustomerID = "C" };
            context.Customers.Add(c);
            context.Customers.Add(c);
            context.Customers.Attach(c);
            new CustomerDemographic { CustomerTypeID = "CD" }.Customers.Add(c);
            Assert.AreEqual(1, context.Customers.Count);
        }
   

    [TestMethod]
    public void MultipleRelationAddAttachOfSameObject()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Customer { CustomerID = "C" };
            var cd = new CustomerDemographic { CustomerTypeID = "CD" };
            c.CustomerDemographics.Add(cd);
            c.CustomerDemographics.Add(cd);
            c.CustomerDemographics.Attach(cd);
            Assert.AreEqual(1, c.CustomerDemographics.Count);
            Assert.AreEqual(1, cd.Customers.Count);
        }
   

    [TestMethod]
    public void DeleteCascade()
    {
        using (var context = new NorthwindClientContext())
        {
            var o = new Order();
            var od = new OrderDetail { Order = o };
            Assert.AreEqual(ObjectState.Detached, o.ChangeTracker.State);
            Assert.AreEqual(ObjectState.Detached, od.ChangeTracker.State);
            context.Orders.Add(o);
            Assert.AreEqual(ObjectState.Added, o.ChangeTracker.State);
            Assert.AreEqual(ObjectState.Added, od.ChangeTracker.State);
            context.Orders.Remove(o);
            Assert.AreEqual(ObjectState.Detached, o.ChangeTracker.State);
            Assert.AreEqual(ObjectState.Detached, od.ChangeTracker.State);
        }
   

 
   [TestMethod]
    public void DeleteCascade2()
    {
        using (var context = new NorthwindClientContext())
        {
            var o = new Order();
            var od = new OrderDetail { Order = o, Product = context.Products.AsQueryable().First(), Discount = 0, Quantity = 1, UnitPrice = 10 };
            context.Orders.Add(o);
            context.SaveChanges();
            var od2 = new OrderDetail { Order = o };
            context.Orders.Remove(o);
            Assert.AreEqual(ObjectState.Deleted, o.ChangeTracker.State);
            Assert.AreEqual(ObjectState.Deleted, od.ChangeTracker.State);
            Assert.AreEqual(ObjectState.Detached, od2.ChangeTracker.State);
            context.SaveChanges();
        }
   

    [TestMethod]
    public void AttachRelationInLoad()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { Id = 1 };
            context.Categories.Attach(c);
            var p = new Product { CategoryID = 1 };
            context.Products.Attach(p);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
        }
   

    [TestMethod]
    public void AttachRelationInLoad2()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { Id = 1 };
            context.Categories.Add(c);
            var p = new Product { CategoryID = 1 };
            context.Products.Add(p);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
        }
   

    [TestMethod]
    public void AttachRelationInLoad3()
    {
        using (var context = new NorthwindClientContext())
        {
            var p = new Product { CategoryID = 1 };
            context.Products.Attach(p);
            var c = new Category { Id = 1 };
            context.Categories.Attach(c);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
        }
   

    [TestMethod]
    public void AttachRelationInLoad4()
    {
        using (var context = new NorthwindClientContext())
        {
            var p = new Product { CategoryID = 1 };
            context.Products.Add(p);
            var c = new Category { Id = 1 };
            context.Categories.Add(c);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
        }
   

    [TestMethod]
    public void AttachRelationInLoad5()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Products.AsQueryable().ToList();
            var c = context.Categories.AsQueryable().First();
            Assert.AreEqual(context.Products.Where(p => p.CategoryID == c.Id).Count(), c.Products.Count);
            foreach (var p in context.Products.Where(p => p.CategoryID == c.Id))
            {
                Assert.AreEqual(c, p.Category);
                Assert.IsTrue(c.Products.Contains(p));
            }
        }
   

    [TestMethod]
    public void AttachRelationInLoad6()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = context.Categories.AsQueryable().First();
            context.Products.AsQueryable().ToList();
            Assert.AreEqual(context.Products.Where(p => p.CategoryID == c.Id).Count(), c.Products.Count);
            foreach (var p in context.Products.Where(p => p.CategoryID == c.Id))
            {
                Assert.AreEqual(c, p.Category);
                Assert.IsTrue(c.Products.Contains(p));
            }
        }
   

    [TestMethod]
    public void AttachRelationInLoad7()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Products.AsQueryable().ToList();
            var c = context.Categories.AsQueryable().Include(Category.PRODUCTS_NAME).First();
            Assert.AreEqual(context.Products.Where(p => p.CategoryID == c.Id).Count(), c.Products.Count);
            foreach (var p in context.Products.Where(p => p.CategoryID == c.Id))
            {
                Assert.AreEqual(c, p.Category);
                Assert.IsTrue(c.Products.Contains(p));
            }
        }
   

    [TestMethod]
    public void AttachRelationInLoad8()
    {
        using (var context = new NorthwindClientContext())
        {
            context.Products.AsQueryable().ToList();
            var c = new Category { Id = 1 };
            context.Categories.Attach(c);
            var c2 = context.Categories.AsQueryable().Include(Category.PRODUCTS_NAME).First();
            Assert.AreEqual(c, c2);
            Assert.AreEqual(context.Products.Where(p => p.CategoryID == c.Id).Count(), c.Products.Count);
            foreach (var p in context.Products.Where(p => p.CategoryID == c.Id))
            {
                Assert.AreEqual(c, p.Category);
                Assert.IsTrue(c.Products.Contains(p));
            }
        }
   

    [TestMethod]
    public void AttachRelationInLoad9()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { CategoryName = "cn" };
            var p = new Product { ProductName = "pn", Category = c };
            context.Categories.Add(c);
            Assert.AreEqual(1, context.Products.Count);
            context.SaveChanges();
            Product p2;
            using (var context2 = new NorthwindClientContext())
            {
                var c2 = context2.Categories.AsQueryable().Include(Category.PRODUCTS_NAME).Where(c3 => c3.Id == c.Id).First();
                p2 = new Product { ProductName = "pn2" };
                c2.Products.Add(p2);
                context2.SaveChanges();
            }
            var c4 = context.Categories.AsQueryable().Include(Category.PRODUCTS_NAME).Where(c3 => c3.Id == c.Id).First();
            Assert.AreEqual(c, c4);
            Assert.IsTrue(c.Products.Contains(p));
            Assert.IsFalse(c.Products.Contains(p2));
            var p3 = c.Products.First(p4 => p4.ProductID == p2.ProductID);
            Assert.IsTrue(c.Products.Contains(p3));
            Assert.IsTrue(context.Products.Contains(p3));
            context.Products.Remove(p3);
            context.Products.Remove(p);
            context.Categories.Remove(c);
            Assert.AreEqual(c.Id, p.CategoryID);
            Assert.AreEqual(c.Id, p3.CategoryID);
            context.SaveChanges();
        }
   

    [TestMethod]
    public void AttachRelationInLoad10()
    {
        using (var context = new NorthwindClientContext())
        {
            var p = context.Products.AsQueryable().First();
            context.Orders.Attach(new Order());
            var od = context.OrderDetails.AsQueryable().Where(od2 => od2.ProductID == p.ProductID).First();
            var p2 = (from p3 in context.Products.AsQueryable().Include(Product.CATEGORY_NAME).Include(string.Concat(Product.ORDERDETAILS_NAME, ".", OrderDetail.ORDER_NAME))
                      where p3.ProductID == p.ProductID
                      select p3).First();
            Assert.AreEqual(p, p2);
            Assert.IsNotNull(p.Category);
            Assert.AreEqual(1, context.Categories.Count);
            Assert.AreEqual(p.OrderDetails.Count, context.OrderDetails.Count);
            foreach (var od2 in p.OrderDetails)
                Assert.IsNotNull(od2.Order);
            Assert.AreEqual(p.OrderDetails.Select(od3 => od3.Order).Distinct().Count() + 1, context.Orders.Count);
            Assert.AreNotEqual(0, context.Orders.Count);
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void AddSameEntityInDifferentContext()
    {
        var p = new Product();
        using (var context1 = new NorthwindClientContext())
        {
            context1.Products.Add(p);
            using (var context2 = new NorthwindClientContext())
            {
                context2.Products.Add(p);
            }
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void AddSameEntityFromServiceInDifferentContext()
    {
        Product p;
        using (var context1 = new NorthwindClientContext())
        {
            p = context1.Products.AsQueryable().First();
            using (var context2 = new NorthwindClientContext())
            {
                context2.Products.Add(p);
            }
        }
   

    [TestMethod]
    public void AddSameEntityInDifferentContextWithDispose()
    {
        var p = new Product();
        using (var context1 = new NorthwindClientContext())
        {
            context1.Products.Add(p);
        }
        using (var context2 = new NorthwindClientContext())
        {
            context2.Products.Add(p);
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void AddSameEntityInDifferentContextWithDisposeAndConflicts()
    {
        var c = new Category { Id = 1 };
        var p1 = new Product { ProductID = 1, Category = c };
        var p2 = new Product { ProductID = 2 , CategoryID = 1};
        using (var context1 = new NorthwindClientContext())
        {
            context1.Categories.Attach(c);
            context1.Products.Attach(p2);
            Assert.AreEqual(2, context1.Products.Count);
            Assert.AreEqual(2, c.Products.Count);
            Assert.AreEqual(c, p2.Category);
        }
        using (var context2 = new NorthwindClientContext())
        {
            var p3 = new Product { ProductID = 1 , CategoryID = 1};
            context2.Products.Attach(p3);
            context2.Categories.Attach(c);
        }
    } 

    [TestMethod]
    public void LoadFromOneToMany()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = context.Categories.AsQueryable().First();
            Assert.AreEqual(0, c.Products.Count);
            Assert.AreEqual(0, context.Products.Count);
            var ps = c.LoadProducts();
            Assert.AreNotEqual(0, ps.Count);
            Assert.AreEqual(ps.Count, c.Products.Count);
            Assert.AreEqual(ps.Count, context.Products.Count);
            foreach (var p in ps)
                Assert.AreEqual(c, p.Category);
        }
   

    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void LoadFromOneToManyDetached()
    {
        Category c;
        using (var context = new NorthwindClientContext())
        {
            c = context.Categories.AsQueryable().First();
        }
        var ps = c.LoadProducts();
   

    [TestMethod]
    public void LoadFromManyToOne()
    {
        using (var context = new NorthwindClientContext())
        {
            var p = context.Products.AsQueryable().First();
            Assert.IsNull(p.Category);
            Assert.AreEqual(0, context.Categories.Count);
            var c = p.LoadCategory();
            Assert.IsNotNull(c);
            Assert.AreEqual(c, p.Category);
            Assert.AreEqual(1, context.Categories.Count);
            Assert.AreEqual(1, c.Products.Count);
            Assert.AreEqual(p, c.Products[0]);
        }
   

    [TestMethod]
    public void LoadFromManyToMany()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = (from cust in context.Customers.AsQueryable()
                     where cust.CustomerID == "VINET"
                     select cust).First();
            Assert.AreEqual(0, c.CustomerDemographics.Count);
            Assert.AreEqual(0, context.CustomerDemographics.Count);
            c.LoadCustomerDemographics();
            Assert.AreNotEqual(0, c.CustomerDemographics.Count);
            Assert.AreEqual(c.CustomerDemographics.Count, context.CustomerDemographics.Count);
        }
   

    [TestMethod]
    public void LoadFromOneToOne()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = context.Customers.AsQueryable().First();
            Assert.IsNull(c.Member);
            Assert.AreEqual(0, context.Members.Count);
            var m = c.LoadMember();
            Assert.IsNotNull(c.Member);
            Assert.AreEqual(1, context.Members.Count);
            Assert.AreEqual(c, m.Customer);
        }
   

    [TestMethod]
    public void OfType()
    {
        using (var context = new NorthwindClientContext())
        {
            var employeesInActivity = context.Employees.AsQueryable().OfType<EmployeeInActivity>().ToList();
            Assert.AreEqual(employeesInActivity.Count, context.Employees.Count);
            Assert.AreEqual(employeesInActivity.Count, context.EmployeeInActivities.Count);
            Assert.AreEqual(0, context.OutEmployees.Count);
        }
   

    [TestMethod]
    public void AddIntoBaseEntitySet()
    {
        using (var context = new NorthwindClientContext())
        {
            var firedEmployee = new FiredEmployee();
            context.Employees.Attach(firedEmployee);
            Assert.AreEqual(1, context.Employees.Count);
            Assert.AreEqual(1, context.OutEmployees.Count);
            Assert.AreEqual(1, context.FiredEmployees.Count);
        }
    }

    [TestMethod]
    public void GetAllEntitiesWithInheritance()
    {
        using (var context = new NorthwindClientContext())
        {
            var l = context.Employees.AsQueryable().ToList();
            Assert.AreEqual(l.Count, context.Employees.Count);
            Assert.AreEqual(l.OfType<OutEmployee>().Count(), context.OutEmployees.Count);
            Assert.AreEqual(l.OfType<FiredEmployee>().Count(), context.FiredEmployees.Count);
            Assert.AreNotEqual(0, context.FiredEmployees.Count);
        }
    } 

 

    [TestMethod]
    public void Detach()
    {
        using (var context = new NorthwindClientContext())
        {
            var c = new Category { Id = 1 };
            context.Categories.Attach(c);
            var p = new Product { CategoryID = 1 };
            context.Products.Attach(p);
            Assert.AreEqual(c, p.Category);
            context.Categories.Detach(c);
            Assert.IsNull(p.Category);
            Assert.AreEqual(c.Id, p.CategoryID);
        }
   

    [TestMethod]
    public void UpdateFK()
    {
        using (var context = new NorthwindClientContext())
        {
            var c1 = new Category { Id = 1 };
            context.Categories.Attach(c1);
            var c2 = new Category { Id = 2 };
            context.Categories.Attach(c2);
            var p = new Product { CategoryID = 1 };
            context.Products.Attach(p);
            Assert.AreEqual(c1, p.Category);
            Assert.AreEqual(1, c1.Products.Count);
            p.CategoryID = 2;
            Assert.AreEqual(c2, p.Category);
            Assert.AreEqual(0, c1.Products.Count);
            Assert.AreEqual(1, c2.Products.Count);
        }
    }
}

I used this EDM for my tests:

image

Moreover, with self-tracking default template the generated SQL update includes all columns even if only one property changed. With my implementation the update will contain only the modified column. Note that this is not true for complex types. If one complex type’s property changes, all the columns mapped on this complex type will be included in the update. It is an EF limitation.

To realize it, I add a list of string ModifiedProperties in ObjectChangeTracker class (I update the self-tracking types T4 template). Then I also change the self-tracking context T4 template. I replace:

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

by

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

If you want to learn more about my templates, I will speak about this for next French Microsoft Tech Days in Paris and for the confoo in Montreal (also in French).

Data oriented WCF service Agile with EF4 and a “sort of” provider LINQ

I use a different approach than Julie’s one for EF Agile development. Indeed, I use here Self-Tracking Entities and I massively use T4.

The first step is to create a project DAL in which we will add an edmx.

In my sample, I use Northwind DataBase with only the Customers, Orders and [Order Details] tables.

image

When it’s done, we will add a new item of type ADO.NET Self-Tracking Entity Generator to our project.

It will add two T4 templates to our project:

  • one with entities + some classes and interfaces for Self-Tracking

image

  • one with the context + a static class with extension methods.

We will move our entities template into another project Entities.

Then we have to add the Entities reference in our DAL project. We will also set the edmx path in our templates.

Then, we will create a new project Repositories. In this project, we will define an interface INorthwindRepository.

image

We can see the redundancy of INorthwindRepository. That’s why, in my sample, these interfaces (INorthwindRepository and IEntitySet) are also generated with a T4 template which uses my edmx. What is great here is the fact that if we change our data model (my edmx), we have no more code to write. We just have to regenerate all my T4 templates.

Now, we will go back to my context. It has to implement INorthwindRepository. For this, we will update my T4 template to have this:

image

For WCF, we need a contract. So I will add two new projects: Services and ServiceContracts (with T4).

image

image

So now, if you already have the T4 templates (which are independent of the project), you wrote 0 lines of code!

We can see the assemblies independence with EF (except for the DAL of course) :

image

I wanted to use Unity. To do it, I use Alexey Zakharov approach. So I integrate his WCFFacility project and classes Bootstrapper and UnityServiceLocatorAdapter in my WCFService (web project host). After creating the svc and the config file, my service is finished!

 

Now in the client tier, I wanted to use a “sort of” provider LINQ. For this, I inspired from one of my old demo made for ALT.NET French community.

We will create a Client.LINQ project in which we will define two classes: ClientLINQ and MyQueryable.

public static class ClientLINQ
{
    public static MyQueryable<T> Where<T>(this MyQueryable<T> source, Expression<Func<T, bool>> where)
    {
        source.WhereValue = string.Concat(source.WhereValue ?? "", where.Body.ToString().Replace(string.Format("{0}.", where.Parameters[0].Name), "it.").Replace("\"", "'").Replace("||", " OR ").Replace("&&", " AND "));
        return source;
    }

 

    public static MyQueryable<T> OrderBy<T, T2>(this MyQueryable<T> source, Expression<Func<T, T2>> orderBy)
    {
        source.OrderByValue = orderBy.Body.ToString().Replace(string.Format("{0}.", orderBy.Parameters[0].Name), "it.");
        return source;
    }

 

    public static MyQueryable<T> Include<T>(this MyQueryable<T> source, string include)
    {
        source.IncludeValues.Add(include);
        return source;
    }

 

    public static MyQueryable<T> Skip<T>(this MyQueryable<T> source, int number)
    {
        source.SkipValue = number;
        return source;
    }

 

    public static MyQueryable<T> Take<T>(this MyQueryable<T> source, int number)
    {
        source.TakeValue = number;
        return source;
    }

 

    public static T FirstOrDefault<T>(this MyQueryable<T> source)
    {
        source.TakeValue = 1;
        return source.AsEnumerable().FirstOrDefault();
    }

 

    public static T First<T>(this MyQueryable<T> source)
    {
        source.TakeValue = 1;
        return source.AsEnumerable().First();
    }

 

    public static MyQueryable<T> ToMyQueryable<T>(this IEnumerable<T> source)
    {
        var value = source as MyQueryable<T>;
        if (value == null)
            value = new MyQueryable<T>(source);
        return value;
    }
}

public class MyQueryable<T> : IEnumerable<T>
{
    public MyQueryable()
    {
    }
    public MyQueryable(IEnumerable<T> enumerable)
    {
        Enumerable = enumerable;
    }

 

    public IEnumerable<T> Enumerable { get; set; }

 

    public IEnumerator<T> GetEnumerator()
    {
        return Enumerable.GetEnumerator();
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

 

    private bool _allEntities = true;
    public bool AllEntities
    {
        get { return _allEntities; }
        internal set
        {
            _allEntities = value;
            if (value)
            {
                IncludeValues.Clear();
                WhereValue = null;
                OrderByValue = null;
                SkipValue = null;
                TakeValue = null;
            }
        }
    }
    private ObservableCollection<string> _includeValues;
    public ObservableCollection<string> IncludeValues
    {
        get
        {
            if (_includeValues == null)
            {
                _includeValues = new ObservableCollection<string>();
                _includeValues.CollectionChanged += (sender, e) =>
                    {
                        if (e.Action == NotifyCollectionChangedAction.Add)
                            AllEntities = false;
                    };
            }
            return _includeValues;
        }
    }
    private string _whereValue;
    public string WhereValue
    {
        get { return _whereValue; }
        internal set
        {
            if (value != null)
                AllEntities = false;
            _whereValue = value;
        }
    }
    private string _orderByValue;
    public string OrderByValue
    {
        get { return _orderByValue; }
        internal set
        {
            if (value != null)
                AllEntities = false;
            _orderByValue = value;
        }
    }
    private int? _skipValue;
    public int? SkipValue 
    {
        get { return _skipValue; }
        internal set
        {
            if (value.HasValue)
                AllEntities = false;
            _skipValue = value;
        }
    }
    private int? _takeValue;
    public int? TakeValue 
    {
        get { return _takeValue; }
        internal set
        {
            if (value.HasValue)
                AllEntities = false;
            _takeValue = value;
        }
    }
}

Then, we will create our client project (a Test project in my sample). In this project, I add Entities project and Client.LINQ references. Client.LINQ is not mandatory. I can also duplicate my entities code but in order to have Self-Tracking, I can’t use the default generated code. Then, we will add the WCF service reference. To use my “sort of” provider LINQ, I need some MyQueryable of my entities. Here also, (I hope that you guess it), we can use a T4 template.

public partial class NorthwindClientContext
{     
      private INorthwindService _service;

      public NorthwindClientContext(INorthwindService service)
      {
            _service = service;
     
  
      public MyQueryable<Customer> Customers
      {
            get
            {
                  var value = new MyQueryable<Customer>();
                  value.Enumerable = GetCustomers(value);
                  return value;
            }
      }
      private IEnumerable<Customer> GetCustomers(MyQueryable<Customer> myQueryable)
      {
            IEnumerable<Customer> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllCustomers();
            else
                  value = _service.GetCustomers(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
     

      public MyQueryable<Order> Orders
      {
            get
            {
                  var value = new MyQueryable<Order>();
                  value.Enumerable = GetOrders(value);
                  return value;
            }
      }
      private IEnumerable<Order> GetOrders(MyQueryable<Order> myQueryable)
      {
            IEnumerable<Order> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllOrders();
            else
                  value = _service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
     

      public MyQueryable<OrderDetail> OrderDetails
      {
            get
            {
                  var value = new MyQueryable<OrderDetail>();
                  value.Enumerable = GetOrderDetails(value);
                  return value;
            }
      } 
      private IEnumerable<OrderDetail> GetOrderDetails(MyQueryable<OrderDetail> myQueryable)
      {
            IEnumerable<OrderDetail> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllOrderDetails();
            else
                  value = _service.GetOrderDetails(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
      }
}

The use of the yield return will postpone the execution and allow MyQueryable properties to be set when GetCustomers / GetOrders / GetOrderDetails methods are called.

So the following code:

var order = (from o in new NorthwindClientContext(service).Orders.Include("Customer").Include("OrderDetails")
             where o.ShipCity == "PARIS"
             orderby o.OrderDate
             select o).Skip(2).First();

will call this method

_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)

with these parameters:

image

wich will generate the following SQL query:

SELECT
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C1] AS [C1],
[Project1].[OrderID1] AS [OrderID1],
[Project1].[ProductID] AS [ProductID],
[Project1].[UnitPrice] AS [UnitPrice],
[Project1].[Quantity] AS [Quantity],
[Project1].[Discount] AS [Discount]
FROM ( SELECT 
      [Limit1].[OrderID] AS [OrderID], 
      [Limit1].[CustomerID1] AS [CustomerID], 
      [Limit1].[EmployeeID] AS [EmployeeID], 
      [Limit1].[OrderDate] AS [OrderDate], 
      [Limit1].[RequiredDate] AS [RequiredDate], 
      [Limit1].[ShippedDate] AS [ShippedDate], 
      [Limit1].[ShipVia] AS [ShipVia], 
      [Limit1].[Freight] AS [Freight], 
      [Limit1].[ShipName] AS [ShipName], 
      [Limit1].[ShipAddress] AS [ShipAddress], 
      [Limit1].[ShipCity] AS [ShipCity], 
      [Limit1].[ShipRegion] AS [ShipRegion], 
      [Limit1].[ShipPostalCode] AS [ShipPostalCode], 
      [Limit1].[ShipCountry] AS [ShipCountry], 
      [Limit1].[CustomerID2] AS [CustomerID1], 
      [Limit1].[CompanyName] AS [CompanyName], 
      [Limit1].[ContactName] AS [ContactName], 
      [Limit1].[ContactTitle] AS [ContactTitle], 
      [Limit1].[Address] AS [Address], 
      [Limit1].[City] AS [City], 
      [Limit1].[Region] AS [Region], 
      [Limit1].[PostalCode] AS [PostalCode], 
      [Limit1].[Country] AS [Country], 
      [Limit1].[Phone] AS [Phone], 
      [Limit1].[Fax] AS [Fax], 
      [Extent3].[OrderID] AS [OrderID1], 
      [Extent3].[ProductID] AS [ProductID], 
      [Extent3].[UnitPrice] AS [UnitPrice], 
      [Extent3].[Quantity] AS [Quantity], 
      [Extent3].[Discount] AS [Discount], 
      CASE WHEN ([Extent3].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
      FROM   (SELECT TOP (1) [Filter1].[OrderID], [Filter1].[CustomerID1], [Filter1].[EmployeeID], [Filter1].[OrderDate], [Filter1].[RequiredDate], [Filter1].[ShippedDate], [Filter1].[ShipVia], [Filter1].[Freight], [Filter1].[ShipName], [Filter1].[ShipAddress], [Filter1].[ShipCity], [Filter1].[ShipRegion], [Filter1].[ShipPostalCode], [Filter1].[ShipCountry], [Filter1].[CustomerID2], [Filter1].[CompanyName], [Filter1].[ContactName], [Filter1].[ContactTitle], [Filter1].[Address], [Filter1].[City], [Filter1].[Region], [Filter1].[PostalCode], [Filter1].[Country], [Filter1].[Phone], [Filter1].[Fax]
            FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID1], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], [Extent2].[CustomerID] AS [CustomerID2], [Extent2].[CompanyName] AS [CompanyName], [Extent2].[ContactName] AS [ContactName], [Extent2].[ContactTitle] AS [ContactTitle], [Extent2].[Address] AS [Address], [Extent2].[City] AS [City], [Extent2].[Region] AS [Region], [Extent2].[PostalCode] AS [PostalCode], [Extent2].[Country] AS [Country], [Extent2].[Phone] AS [Phone], [Extent2].[Fax] AS [Fax], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
                  FROM  [dbo].[Orders] AS [Extent1]
                  LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
                  WHERE [Extent1].[ShipCity] = 'PARIS'
            )  AS [Filter1]
            WHERE [Filter1].[row_number] > 2
            ORDER BY [Filter1].[OrderDate] ASC ) AS [Limit1]
      LEFT OUTER JOIN [dbo].[Order Details] AS [Extent3] ON [Limit1].[OrderID] = [Extent3].[OrderID]
)  AS [Project1]

ORDER BY [Project1].[OrderDate] ASC, [Project1].[OrderID] ASC, [Project1].[CustomerID1] ASC, [Project1].[C1] ASC

If we comment the Include, the SQL query is more readable Smile :

SELECT TOP (1)
[Filter1].[OrderID] AS [OrderID],
[Filter1].[CustomerID] AS [CustomerID],
[Filter1].[EmployeeID] AS [EmployeeID],
[Filter1].[OrderDate] AS [OrderDate],
[Filter1].[RequiredDate] AS [RequiredDate],
[Filter1].[ShippedDate] AS [ShippedDate],
[Filter1].[ShipVia] AS [ShipVia],
[Filter1].[Freight] AS [Freight],
[Filter1].[ShipName] AS [ShipName],
[Filter1].[ShipAddress] AS [ShipAddress],
[Filter1].[ShipCity] AS [ShipCity],
[Filter1].[ShipRegion] AS [ShipRegion],
[Filter1].[ShipPostalCode] AS [ShipPostalCode],
[Filter1].[ShipCountry] AS [ShipCountry]
FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
      FROM [dbo].[Orders] AS [Extent1]
      WHERE [Extent1].[ShipCity] = 'PARIS'
)  AS [Filter1]
WHERE [Filter1].[row_number] > 2

ORDER BY [Filter1].[OrderDate] ASC

We can find our WHERE City = ‘Paris’, our WHERE row_number > 2 (for the Skip), out ORDER BY OrderDate and our TOP  1 (for the First).

 

Take care, this “sort of” provider LINQ is a POC. It is not finished. For example, it doesn’t support variables or the new operator (for date for example).

 

What happens if we integrate methods non supported by ClientLINQ?

It’s ok! Smile

LINQ To Object will be used.

For example, the following LINQ query:

var customerInfos = (from o in new NorthwindClientContext(service).Orders.Include("Customer")
                     where o.ShipCity == "PARIS"
                     orderby o.OrderDate
                     group o by o.Customer into
                     select new { g.Key.CompanyName, g.Key.ContactName, OrdersCount = g.Count() }).ToList();

will call this method

_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)

with these parameters:

image

which will generate the following SQL query:

SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry],
[Extent2].[CustomerID] AS [CustomerID1],
[Extent2].[CompanyName] AS [CompanyName],
[Extent2].[ContactName] AS [ContactName],
[Extent2].[ContactTitle] AS [ContactTitle],
[Extent2].[Address] AS [Address],
[Extent2].[City] AS [City],
[Extent2].[Region] AS [Region],
[Extent2].[PostalCode] AS [PostalCode],
[Extent2].[Country] AS [Country],
[Extent2].[Phone] AS [Phone],
[Extent2].[Fax] AS [Fax]
FROM  [dbo].[Orders] AS [Extent1]
LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE [Extent1].[ShipCity] = 'PARIS'
ORDER BY [Extent1].[OrderDate] ASC

As you can see, no trace of group by. However, the result integrates it (with LINQ To Object).

 

In this post, I wanted to present my “sort of'” LINQ provider but the real goal of this post is to persuade you (and I’m sure you are Smile) of the EF / T4 couple productivity gain. Indeed,

  • your templates are reusable in different projects. You just have to update the edmx path and to let Visual Studio regenerate T4 templates code.
  • If you haven't already written the template you need, the dev time is not proportional to the number of entities which implies very quickly a productivity gain compare to a “classic” dev.

4 in 1: EF won’t fire DBAs, Velocity, SQL CLR and SQL Dependency

Imagine that we have a table Cars with a PK Id (nvarchar(8)) based on the following regex pattern [1-9][0-9]{1,2}[A-Z]{2,3}[0-9]{2}. The last two numbers are a region code.

The increasement is done like this: 10AA[region], 11AA[region], …, 999AA[region], 10AB[region], …, 999ZZ[region], 10AAA[region], …, 999ZZZ[region]

This table can be huge.

In a first time, we will write a LINQ To Entities query to get regions with 3 letters:

var q1 = (from c in context.Cars
          where c.Id.Contains("AAA")
          select c.Id.Substring(c.Id.Length - 2)).Distinct();

The SQL generated query is the following:

SELECT
[Distinct1].[C1] AS [C1]
FROM ( SELECT DISTINCT

      SUBSTRING([Extent1].[ Id ], ((LEN([Extent1].[ Id ])) - 2) + 1, (LEN([Extent1].[ Id ])) - ((LEN([Extent1].[ Id ])) - 2)) AS [C1]
      FROM [dbo].[Cars] AS [Extent1]
      WHERE [Extent1].[ Id ] LIKE N'%AAA%'
)  AS [Distinct1]

If I write it myself, I would probably write this:

SELECT DISTINCT
      RIGHT(Id, 2) AS [C1]
      FROM [Cars]
      WHERE [ Id ] LIKE N'%AAA%'

Note that the execution plan is the same!

Imagine that the DBA wants that I use the RIGHT function. With EF4, we can do it.

The Right doesn’t exist in C# but it does in ESQL. So we will use CSDL Functions.

In the CSDL, we will add the following code:

<Function Name="GetRegion" ReturnType="String">
  <Parameter Name="car" Type="Self.Car" />
  <DefiningExpression>
    Right(car.Id, 2)
  </DefiningExpression>
</Function>

Then,we will add an extension method to be able to use it in our LINQ To Entities queries:

public static class CarExtension
{
    [EdmFunction("CarsModel", "GetRegion")] 
    public static string GetRegion(this Car car)
    {
        throw new NotImplementedException("Only used by LINQ To Entities");
    }
}

I now can write my query like this:

var q1 = (from c in context.Cars
          where c.Id.Contains("AAA")
          select c.GetRegion()).Distinct();

This LINQ query is translated as the following SQL query:

SELECT
[Distinct1].[C1] AS [C1]
FROM ( SELECT DISTINCT
      RIGHT([Extent1].[ Id ], 2) AS [C1]
      FROM [dbo].[Cars] AS [Extent1]
      WHERE [Extent1].[ Id ] LIKE N'%AAA%'
)  AS [Distinct1]

Cool!

Now I want to get the last Id per region. I don’t think that there is a clean way to do it with SQL. Indeed, I don’t think that we can use Regex in SQL Server and we need it to determine which is the last id.

So to do this, I will use a LINQ To Object query. The “normal” way can be to write this:

var qL2E = from c in context.Cars
           orderby c.GetRegion()
           select c.Id;
var qL2O = from id in qL2E.AsEnumerable()
           group id by id.Substring(id.Length - 2) into g
           select new
           {
               Region = g.Key,
               Id = (from id in g
                     let letters = Regex.Match(id, "[A-Z]{2,3}")
                     orderby letters.Length descending, letters descending, Regex.Match(id, "^[0-9]{2,3}") descending
                     select id).FirstOrDefault()
           }.ToDictionary(id => id.Region, id => id.Id);

However, with this way, we will probably have an OutOfMemoryException. Indeed, with this query, all the Cars records are loaded in memory and we can have hundreds millions of rows.

In a first time, we will try a dichotomous approach.

var ids = (from c in context.Cars
           select c.GetRegion()).Distinct().AsEnumerable().ToDictionary(region => region, region => GetLastId(region, "", (new[] { "" }.Union(Enumerable.Range('A', 26).Select(i => char.ConvertFromUtf32(i)))), 0));

private static string GetLastId(string region, string letters, IEnumerable<string> possibleChars, int index)
{
    if (!possibleChars.Skip(1).Any()) // Count() == 1
    {
        letters += possibleChars.First();
        if (++index == 3)
        {
            using (var context = new CarsContainer())
            {
                return
                    (from c in context.Cars
                     where c.Id.EndsWith(letters + region)
                     orderby c.Id.IndexOf(letters) descending, c.Id descending
                     select c.Id).FirstOrDefault();
            }
        }
        return GetLastId(region, letters, Enumerable.Range('A', 26).Select(i => char.ConvertFromUtf32(i)), index);
    }
    else
    {
        var possibleCharsList = possibleChars.ToList();
        int middle = possibleCharsList.Count / 2;
        using (var context = new CarsContainer())
        {
            string idBeginning = string.Format("10{0}{1}{2}", letters, possibleCharsList[middle], "AA".Substring(index));
            if ((from c in context.Cars
                 where c.Id.StartsWith(idBeginning) && c.Id.EndsWith(region)
                 select c.Id).Any())
                return GetLastId(region, letters, possibleCharsList.Skip(middle), index);
            return GetLastId(region, letters, possibleCharsList.Take(middle), index);
        }
    }
}

The execution of this query is very slow (26 minutes and 18 seconds in my test with 562 012 347 records).

So what can we do?

The CPU is used by the DB at 100% and the slowly is only because of the DB. So it useless to imagine to parallelize our code.

We can imagine to resize the Data Base server but it is not the goal of this post.

In our case, the DBA (I specify that I am not a DBA) will probably want to change the Cars table schema in order to split into 3 columns the Id. However, in a lot of cases, it can be interesting to keep the Id column, which keeps the PK. Indeed, imagine that we have some existing applications. The idea is to add these columns with no impact on them.

So we will have four columns (all not nullable) :

  • Id
  • Number
  • Letters
  • Region

To set them, we will use CLR functions:

public class CarsFunction
{
    [SqlFunction]
    public static SqlInt16 GetNumber(SqlString id)
    {
        return new SqlInt16(short.Parse(Regex.Match(id.Value, "^[0-9]{2,3}").Value));
   

    [SqlFunction]
    public static SqlString GetLetters(SqlString id)
    {
        return new SqlString(Regex.Match(id.Value, "[A-Z]{2,3}").Value);
   

    [SqlFunction]
    public static SqlInt16 GetRegion(SqlString id)
    {
        return new SqlInt16(short.Parse(Regex.Match(id.Value, "[0-9]{2}$").Value));
    }
}

Then, in the DB, we will register the assembly and we will create some SQL functions from previous ones:

CREATE ASSEMBLY CarsFunctionsAssembly
FROM 'D:\documents\visual studio 2010\Projects\CarsFunctions\CarsFunctions\bin\Debug\CarsFunctions.dll'
GO   

CREATE FUNCTION GetNumber(@id AS nvarchar(8)) RETURNS smallint AS EXTERNAL NAME CarsFunctionsAssembly.CarsFunction.GetNumber
GO

CREATE FUNCTION GetLetters(@id AS nvarchar(8)) RETURNS nvarchar(3) AS EXTERNAL NAME CarsFunctionsAssembly.CarsFunction.GetLetters
GO

CREATE
FUNCTION GetRegion(@id AS nvarchar(8)) RETURNS smallint AS EXTERNAL NAME CarsFunctionsAssembly.CarsFunction.GetRegion

Ok, we have the functions, so we can be back on the new columns creation. Instead of creating “normal” columns, we will use computed columns. We can do it with the SSMS designer by setting the Computed Column Specification Formula to dbo.GetNumber(Id) (resp dbo.GetLetters(Id), dbo.GetRegion(Id)). In our case, the idea isn’t to calculate them each time but to keep their values. So we will set the “Is Persisted” property to true.

But there is an issue: the function has to be deterministic. Our function is. However, because it is a CLR function, SQL Server can’t know it. So we will tell it explicitly.

To do this, we will change the CLR functions code like this:

[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static SqlInt16 GetNumber(SqlString id)
{
    return new SqlInt16(short.Parse(Regex.Match(id.Value, "^[0-9]{2,3}").Value));
}

[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static SqlString GetLetters(SqlString id)
{
    return new SqlString(Regex.Match(id.Value, "[A-Z]{2,3}").Value);
}

[SqlFunction(IsDeterministic = true, IsPrecise = true)]
public static SqlInt16 GetRegion(SqlString id)
{
    return new SqlInt16(short.Parse(Regex.Match(id.Value, "[0-9]{2}$").Value));
}

Now, we can persist the three columns.

Be careful, if we create an edmx from the DB, we will have a StoreGeneratedPattern="Computed" on the columns Number, Letters and Region. It implies that in each update, EF, we reload the properties Number, Letters et Region. In our case, the calculation is only done on the key. With Entity Framework, it is not possible to change the value of the key. So, it is useful to change the StoreGeneratedPattern from Computed to Identity.

Cool, we have fixed the INSERT issue.

Let’s now concentrate on the goal: our query.

We don’t need a dichotomous approach, a simple LINQ To Entities query is enough:

var ids = (from c in context.Cars
           group c by c.Region into g
           select new
           {
               Region = g.Key,
               LastId = (from c in g
                         orderby c.Letters.Length descending, c.Letters descending, c.Number descending
                         select c.Id).FirstOrDefault() 
           }).AsEnumerable().ToDictionary(region => region.Region, region => region.LastId);

With this, our query runs in 1 minute and 42 seconds instead of 26 minutes and 18 seconds!

It’s better but it isn’t finished yet.

In the DB, we will set the Cars table cluster on the Region column (instead of the Id column).

In this case, the query execution falls to 12 seconds!

However, be careful! It’s really great in our sample but in the real life, don’t forget to study all the DB query on the table Cars before changing the cluster.

So the DBA role is important in the success of the project.

 

Now, imagine that we want to keep in cache the last id per region. To do it, we can use a static Dictionary. But there are two issues:

  • Imagine a N-Tiers scenario. If our application is deployed on more than one frontal server, it’s a shame to have to initialize the cache for each of them. Moreover, we have to maintain each of them with the last values
  • If another application adds or removes a car in the DB, how to maintain our cache with the last values?

To fix the first issue, we will Velocity. For the second, we will use SQLDependency.

Velocity is a distributed cache which allows a lot of very interesting things like load balancing. This distributed cache will be shared by all the frontal servers. It means that we won”t have to manage one cache per server.

What do we have to do to use Velocity? First point: download it and install it.Smile

When the installation is done, we will add the following references in our project:

  • CacheBaseLibrary.dll
  • CASBase.dll
  • CASMain.dll
  • ClientLibrary.dll
  • FabricCommon.dll

Then, we have to start the Velocity cluster. In this CTP, all administration commands are written with the console “Administration Tool - Microsoft Distributed Cache”. Then we write the following command line:

start-cachecluster

Then, in the config file, we wiil add these lines:

<configSections>
  <
section name="dataCacheClient" type="Microsoft.Data.Caching.DataCacheClientSection, CacheBaseLibrary" allowLocation="true" allowDefinition="Everywhere"/>
</
configSections>
<
dataCacheClient deployment="routing">
  <
localCache isEnabled="true" sync="TTLBased" ttlValue="60000"/>
  <
hosts>
    <
host name="MATTHIEU-PRO" cachePort="22233" cacheHostName="DistributedCacheService"/>
    <
host name="MATTHIEU-LAPTOP1" cachePort="22233" cacheHostName="DistributedCacheService"/>
    <
host name="MATTHIEU-LAPTOP2" cachePort="22233" cacheHostName="DistributedCacheService"/>
    <
host name="MATTHIEU-PC1" cachePort="22233" cacheHostName="DistributedCacheService"/>
    <
host name="MATTHIEU-PC2" cachePort="22233" cacheHostName="DistributedCacheService"/>
  </
hosts>
</
dataCacheClient>

Now we can imagine to query the cache in our application (server tier).

return new DataCacheFactory().GetDefaultCache().GetObjectsInRegion("LastImmatPerRegion").ToDictionary(keyValuePair => short.Parse(keyValuePair.Key), keyValuePair => (string)keyValuePair.Value);

With the data in the cache, the query executes in 30 ms!

Now, we have to fix the last point: initializing the cache and maintaining it.

To initialize it, it’s very easy. We just have to use the above query:

var cache = new DataCacheFactory().GetDefaultCache();

try
{
  
cache.RemoveRegion(LAST_IMMAT_PER_REGION);
}
catch
{
} 
cache.CreateRegion(LAST_IMMAT_PER_REGION, false);
foreach (var car in from c in context.Cars
                    group c by c.Region into g
                    select new
                    {
                        Region = g.Key,
                        LastId = (from c in g
                                  orderby c.Letters.Length descending, c.Letters descending, c.Number descending
                                  select c.Id).FirstOrDefault()
                    })
    cache.Put(car.Region.ToString(), car.LastId, LAST_IMMAT_PER_REGION);

Now to maintain the cache with the last values, it’s harder. My idea is to use a SQLDependency. The issue is the fact that we can’t, a priori, get the modifications with SQLDependency and, of course, I don’t want to refresh all my cache. So my idea is to add a Trigger on the Cars table which will fill a new table (for updates).

To begin, we will create this new table: CarsModificationsTmp with six columns:

  • TmpId (int Identity(1,1), PK),
  • CarId (nvarchar(8), not nullable)
  • Number (smallint, not nullable)
  • Letters (nvarchar(3), not nullable)
  • Region (smallint, not nullable),
  • Deleted (bit, not nullable, default=0)

Then, we have to define triggers on Cars table:

CREATE TRIGGER CarsInserted
ON Cars
FOR INSERT
AS
BEGIN
  DECLARE @Id AS nvarchar(8)
  DECLARE @Number AS smallint
  DECLARE @Letters AS nvarchar(3)
  DECLARE @Region AS smallint
  SELECT @Id = Id, @Number = Number, @Letters = Letters, @REGION = Region FROM Inserted
  INSERT INTO CarsModificationsTmp(CarId, Number, Letters, Region) VALUES(@Id, @Number, @Letters, @Region)
END
GO
 
CREATE TRIGGER CarsDeleted
ON Cars
FOR DELETE
AS
BEGIN
  DECLARE @Region AS smallint
  DECLARE @Id AS nvarchar(8)
  DECLARE @Number AS smallint
  DECLARE @Letters