Living in the Tech Avalanche Generation

A practitioner’s introspective on technology
Archive for February 23rd, 2009

Entity Framework, Repositories, Specifications and Fetching Strategies Part 9.0

Some time ago I started out writing a post that started out with a title of :

“Entity Framework won’t be featuring in my production code for now!”

Clearly you can see I was getting frustrated. These were my initial thoughts in the post:

“As long as the Entity Framework forces me to sprinkle lazy load instructions amongst my business logic, I will not use it. I will continue to follow it’s progress, however as far as production code goes, I will be using NHibernate as it allows me to work in the manner I wish to without making compromises that are not in my estimation reasonable.”

I left that post to ferment a little and eventually went back to the drawing board on getting Fetching Strategies into the EF nonetheless and recently I posted about some success I believed I was having in developing an approach to unifying Lazy and Eager fetching, neither of which have a suitable (for me) out of the box solution baked in.

Let me be clear from the outset, that this approach uses Aspect Oriented techniques (courteousey of PostSharp) and does require developer intervention when it comes to managing your Entity Framework source files, so if you were looking for a POCO solution (much as I wish it were) you won’t find it here.

A well rounded loading story (both eager and lazy), is critical in making the right performance choices when it comes time to writing the business logic that you know will end up causing persistence and my favoured approach to this is a highly configurable an tunable one. Udi demonstrates this using NHibernate and Fetching Strategies.

using (ISession s = sf.OpenSession())
using (ITransaction tx = s.BeginTransaction())
{
    ICustomerToMakePreferred c =
        s.Get<ICustomerToMakePreferred>(customerId);
    c.MakePreferred();
    tx.Commit();
}

If your not familiar with the approach then I suggest you check out Udi’s posts. The code above has expressed it’s fetching intent appropriately by specifying the ICustomerMakePrefered interface for which a FetchingStrategy will have also been implemented where the interface (role) is specified as a generic argument and from there the configuration does the rest.

Currently the Entity Framework provides the very specific and in-line options of ObjectQuery<T>.Include() and .IsLoaded and .Load(). I don’t mind per se having as a choice the option to use the IsLoaded property and Load() method however I don’t wish to use them and have them clutter my entities business logic with infrastructure details. As far as the .Include() function is concerned, that’s easily dealt with and we can wrap that up in our a fetching strategy quite nicely, passing our Repository our intent on eager loading with a Fetching Strategy is not too much work and up until now this is how the FetchingIntention has been initialised.

public class MultiLevelMixedStrategy :
    IFetchingStrategy
{
    private readonly IList<FetchingIntention> _intentions =
        new List<FetchingIntention>();

    public MultiLevelMixedStrategy()
    {
        this.Intentions.Add(new
            FetchingIntention(“Orders”, FetchMode.Eager));
        this.Intentions.Add(new
            FetchingIntention(“Orders.Order_Details”, FetchMode.Eager));
        this.Intentions.Add(new
            FetchingIntention(“Product”, FetchMode.Lazy));
    }

    public IList<FetchingIntention> Intentions
    {
        get { return _intentions; }
    }
}

And we use it in the following manner:

public void Multi_Level_Mixed_Hierarchy_With_Persisting_Test()
{
    EntitiesRepository<Customer, NorthwindEntities> repos =
        new EntitiesRepository<Customer, NorthwindEntities>
        (new NorthwindEntities());

    Specification<Customer> german_cust_spec =
        new Specification<Customer>(c => c.CustomerID == “ALFKI”);

    var german_custs = repos.AllToIList(german_cust_spec, new
                        MultiLevelMixedStrategy());

    foreach (var cust in german_custs)
    {
        cust.Country = “Germany”;
        Console.WriteLine(“The Customer Name is {0}”, cust.CompanyName);
        foreach (var order in cust.Orders)
        {
            order.ShipName = “Battleship Potemkin”;
            Console.WriteLine(“\tThe Order ID is : {0}”, order.OrderID);
            foreach (var orderline in order.Order_Details)
            {
                orderline.Discount = 0.15f;
                Console.WriteLine(“\t\tThe value ordered for Product “ +
                “ID {0} is {1}”, orderline.Product.ProductID,
                 orderline.UnitPrice * orderline.Quantity);
            }
        }
    }
    repos.Save();
}

K. Scott Allen suggested an approach that would do away with the magic strings in the FetchingIntention such as “Orders.Order_Details” and have them replaced with a LINQ Expression something like:

var fi = new FetchingIntention(o => o.Order_Details),
                                 FetchMode.Eager);

The only real problem to deal with in taking this approach was how to accommodate the full object graph of the intention. In the example above it would have proved problematic to express the “Orders.Order_Details” relationship and get the property names correct, when the “o” is an Order as far as the Entity Framework ObjectContext is concerned. I decided to approach this by refactoring the FetchingIntention class and adding the following factory method :

public static FetchingIntention CreateInstance<TRootEntity, TFetchEntity>(
    Expression<Func<TRootEntity, TFetchEntity>> fetch, FetchMode mode)
{
    if (fetch.Parameters.Count > 1)
        throw new ArgumentException
        (“FetchingIntentions support only “ +
            “one parameter in a dynamic expression!”);

    int dot = fetch.Body.ToString().IndexOf(“.”) + 1;
    string includes =
        fetch.Body.ToString().Remove(0, dot);

    _factory_intent =
        new FetchingIntention(includes, mode);

    return _factory_intent;
}

However this alone wasn’t enough to satisfy the problem of expressing the full object graph as a string, such as (”Orders.Order_Details”). To deal with this issue I decided on a fluent interface (simple extension method) to compound FetchingIntentions, it’s listed here:

public static FetchingIntention And(this FetchingIntention original,
                                    FetchingIntention addTo)
{
    FetchingIntention compound_intention;
    FetchMode mode;

    if (original.FetchMode != addTo.FetchMode)
        throw new ArgumentException
            (“FetchingIntention objects must “ +
            “share the same type of fetch mode “ +
            “to be joined together!”);

    compound_intention =
        new FetchingIntention(original.FetchAssociate + “.” +
            addTo.FetchAssociate, original.FetchMode);

    return compound_intention;
}

And a simple example of the usage of our new approach to specifying Fetching intent is:

public void Lambda_Built_Strategy_With_Hierarchies_Test()
{
    EntitiesRepository<Customer, NorthwindEntities> repos =
        new EntitiesRepository<Customer,
        NorthwindEntities>(new NorthwindEntities());

    var custs = repos.AllToIList(new RoleStrategy());

    foreach (var cust in custs)
    {
        Console.WriteLine(“The Customer Name is {0}”, cust.CompanyName);
        foreach (var order in cust.Orders)
        {
            Console.WriteLine(“\tThe Order ID is : {0}”, order.OrderID);
            foreach (var orderline in order.Order_Details)
            {
                Console.WriteLine(“\t\tThe value ordered for Product ID” +
                    “{0} is {1}”, orderline.Product.ProductID,
                    orderline.UnitPrice * orderline.Quantity);
            }
        }
    }
}

When executed and profiled at the Database, the following TSQL can be observed to be executed:

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

This is completely expected as the FetchingStrategy used was the following:

public class RoleStrategy :
    IFetchingStrategy<ICustomerMakePrefered>
{
    private readonly IList<FetchingIntention> _intentions =
        new List<FetchingIntention>();

    public RoleStrategy()
    {
        var order_intention =
            FetchingIntention.CreateInstance
            <Customer, EntityCollection<Order>>
            (c => c.Orders, FetchMode.Eager);

        var order_details_intention = order_intention.And(
            FetchingIntention.CreateInstance
            <Order, EntityCollection<Order_Detail>>
            (o => o.Order_Details, FetchMode.Eager));

        var product_intention = FetchingIntention.CreateInstance
            <Order_Detail, Product>
            (od => od.Product, FetchMode.Lazy);

        this.Intentions.Add(order_intention);
        this.Intentions.Add(order_details_intention);
        this.Intentions.Add(product_intention);
    }

    public IList<FetchingIntention> Intentions
    {
        get { return _intentions; }
    }
}

And also expected therefore (courtesy of the single lazy load intention) was a series of TSQL queries asking for the Product data from the Order_Details lines. Observed from SQL Profiler we can see this behaviour.

exec sp_executesql N‘SELECT
1 AS [C1],
CASE WHEN ([Extent2].[Discontinued] = 0) THEN ‘‘1X’‘ ELSE ‘‘1X0X’‘ END AS [C2],
[Extent2].[ProductID] AS [ProductID],
[Extent2].[ProductName] AS [ProductName],
[Extent2].[SupplierID] AS [SupplierID],
[Extent2].[QuantityPerUnit] AS [QuantityPerUnit],
[Extent2].[UnitPrice] AS [UnitPrice],
[Extent2].[UnitsInStock] AS [UnitsInStock],
[Extent2].[UnitsOnOrder] AS [UnitsOnOrder],
[Extent2].[ReorderLevel] AS [ReorderLevel],
[Extent2].[CategoryID] AS [CategoryID]
FROM  [dbo].[Order Details] AS [Extent1]
INNER JOIN [dbo].[Products] AS [Extent2]
ON [Extent1].[ProductID] = [Extent2].[ProductID]
WHERE ([Extent1].[OrderID] = @EntityKeyValue1) AND ([Extent1].[ProductID] =
@EntityKeyValue2)’,N‘@EntityKeyValue1 int,@EntityKeyValue2 int’,
@EntityKeyValue1=10979,@EntityKeyValue2=27
 

Lazy Loading

The lazy fetching is made possible by introducing a post compiled aspect using PostSharp. By decorating partial classes (that are themselves Entities in a domain model) with an OnMethodBoundaryAspect attribute, we are able to intercept calls to the entity properties that will be called when we would expect lazy loading to occur. For example in Northwind we might have something like the following:

[LazyLoad("Orders", AttributeTargetMembers = @"regex:(Orders)")]
public partial class Customer : IFetchable, ICustomerMakePrefered
{
    private IFetchingStrategy _strategy;

    public void Makeprefered() { }

    public IFetchingStrategy FetchingStrategy
    {
        get { return _strategy; }
        set { _strategy = value; }
    }
}

As stated, the LazyLoad attribute is a OnMethodBoundaryAspect and will weave the lazy loading code into the Getter for the Orders property in the Customer entity. Without delving into the mechanics, I should mention that the intercepting code that gets woven here will not interfere with Entity Framework code that is written without a Fetching Strategy, however this could of course be changed rather easily but it was my preference at the time of spiking this project.

What’s Left?

 
public void Example_Role_Specified_Strategy_Test()
{
    EntitiesRepository<Customer, NorthwindEntities> repos =
        new EntitiesRepository<Customer,
            NorthwindEntities>(new NorthwindEntities());

    var custs = repos.AllToIList<ICustomerMakePrefered>();
}

I did intend to leave the contents of this post for one final post on this subject, including the code that injects FetchingStrategy implementations into the Repository using an IoC container (see above usage - not implementation), however I decided to list the source code now and re-list it when I make subsequent updates.

It’s important to point out that this code is free for you to prod, paw over and mangle at your hearts content,  however it is spiking code and not intended for production without significant further testing and refactoring. Some areas that I plan to clean up are the PostSharp attributes by placing their declarations in the AssemblyInfo file, as opposed to decorating the entities themselves. Also to clean up the code in the attribute itself, I did plan on borrowing Pablo Blamirez code when it became apparent to me recently however working on it in got placed further down in the priority queue for now. 

Download the code

Share/Save/Bookmark

4 comments

Creative Commons Attribution-ShareAlike 2.5 Australia
Creative Commons Attribution-ShareAlike 2.5 Australia