Living in the Tech Avalanche Generation

A practitioner’s introspective on technology

Entity Framework, Fluent Interfaces & Domain Specific Languages Part 2

In the first part of this series I looked at how you might go about building an (incredibly tiny) domain specific language for analysing data. The context I gave was a scenario where project managers were required to work with a continuous stream of data in the form of a known schema. This ‘known’ schema is most commonly used in moving and transforming data between various systems in a domain where the central or end target is a Document Management System. The ‘known’ schema is an agreed format that all systems in this particular industry use to extract and subsequently load. It is common to see the project managers struggling with tools like access to compose queries to analyse the data before or after these ETL processes and hence proposition of a DSL.

In the first part I wasn’t particularly happy with the underlying behaviour of the language statements; we were producing a cascading set of queries, rather than composing a query structure. This time around we look to achieve this compositional affect with a fluent interface that issues a beginning statement and subsequently composes a query until a finalizing command is submitted to indicate that execution should take place against the database. If we use Specifications to compose the predicates in querying the database we can achieve the composition through chaining extension methods and thus build up an execution context from the syntax in our language.

Consider this fluent interface:

public void prints_order_lines_grouped_by_customer_and_orders()
{
    NorthwindLangQuery
    .BeginQuery()
    .Customers()
    .With_An_Id_Equal_To(“ALFKI”)
    .With_Orders_Shipped_From(“Germany”)
    .That()
    .Are()
    .Older_In_Years_By(2)
    .EndCustomerQuery()
    .Print_Customer_Orders_To_Console_Window();
}
which produces the following output:
Order ID: ALFKI
10643
Product ID 28 x 15 units @ total cost = 683.8500
Product ID 39 x 21 units @ total cost = 377.8500
Product ID 46 x 2 units @ total cost = 23.8500
10692
Product ID 63 x 20 units @ total cost = 877.8500
10702
Product ID 3 x 6 units @ total cost = 59.8500
Product ID 76 x 15 units @ total cost = 269.8500
10835
Product ID 59 x 15 units @ total cost = 824.8500
Product ID 77 x 2 units @ total cost = 25.8500
10952
Product ID 6 x 16 units @ total cost = 399.8500
Product ID 28 x 2 units @ total cost = 91.0500
11011
Product ID 58 x 40 units @ total cost = 529.8500

Instead of cascading sub queries that work on an initial database query as with the code shown in part 1, we use the specification pattern to build up predicates that we submit with a single SQL (LINQ To Entities) query to the database. If we pull apart the NorthwindLang query above and follow it method by method here is what it looks like:

public static NorthwindLang BeginQuery()
{
    _customerSpec =
        new Specification<Customer>(c => c == c);

    _orderSpec =
        new Specification<Order>(o => o == o);

    _orderLineSpec =
        new Specification<OrderLine>(ol => ol == ol);
    return new NorthwindLang();
}

The BeginQuery() method simply primes each of our specifications which progressively are built upon in the following chained method calls.

No change required in the method body below as we already know we are dealing with a customer.

public static Specification<Customer> Customers(this NorthwindLang query)
{
    return _customerSpec;
}

Next the specification has a filtering expression to the customer ID appended using the logical AND operator.

public static Specification<Customer> With_An_Id_Equal_To
            (this Specification<Customer> customers, string id)
{
    var cust_id_spec =
        new Specification<Customer>(c => c.CustomerID == id);
    _customerSpec &= cust_id_spec;
    return _customerSpec;
}

The customer orders are also required in the query so we are going to need to setup the Orders specification with it’s required predicate.

public static Specification<Order> With_Orders_Shipped_From
            (this Specification<Customer> customers, string country)
{
    Specification<Order> order_shipcountry_spec =
        new Specification<Order>(o => o.ShipCountry == country);
    _orderSpec &= order_shipcountry_spec;
    return _orderSpec;
}

The next two parts of the query are benign and have no effect on the composition of the query and only play a part in providing a more natural syntax composition for the language.

public static Specification<Order> That
            (this Specification<Order> orders)
{
    return _orderSpec;
}

public static Specification<Order> Are
    (this Specification<Order> orders)
{
    return _orderSpec;
}

Next we add a further Orders specification limiting the result set to orders more than twelve years old.

public static Specification<Order> Older_In_Years_By
            (this Specification<Order> orders, int age)
{
    var date = DateTime.Now.AddYears(-age);
    Specification<Order> order_older_spec =
        new Specification<Order>(o => o.OrderDate < date);
    return _orderSpec &= order_older_spec;
}

Finally we demarcate the end of the query by calling one of the finalization methods that can only be followed by subsequent printing methods.

public static IEnumerable<LangDomain.Customer>
    EndCustomerQuery(this Specification<Order> orders)
{
    return DomainQuery<LangDomain.Customer>
        .CreateQuery(_customerSpec, _orderSpec);
}

This is still all proof of concept stuff and I plan to follow up with putting a harness together with the Fireball code editor API. Here is a further list of some of the other language statements that the fluent interface is capable of producing at the time of this writing.

public void prints_order_lines_grouped_by_customer_and_orders()
{
    NorthwindLangQuery
        .BeginQuery()
        .Customers()
        .With_An_Id_Equal_To(“ALFKI”)
        .With_Orders_Shipped_From(“Germany”)
        .That()
        .Are()
        .Older_In_Years_By(2)
        .EndCustomerQuery()
        .Print_Customer_Orders_To_Console_Window();
}

public void selects_customers_by_id()
{
    var cust = NorthwindLangQuery
        .BeginQuery()
        .Customers()
        .With_An_Id_Equal_To(“ALFKI”)
        .EndCustomerQuery()
        .As_Unique_Where_Customers_Ids_Are_The_Same();

    foreach (var c in cust)
    {
        Console.WriteLine(c.CompanyName);
    }
}

public void selects_orders_specified_by_age_of_orders_with_
    specified_freight_costs()
{
    var query = NorthwindLangQuery
        .BeginQuery()
        .Orders()
        .Older_In_Years_By(12)
        .With()
        .Freight_Costs_More_Than(360)
        .EndOrderQuery();

    foreach (var o in query)
    {
        foreach (var ol in o.Order_Details)
        {
            Console.WriteLine(o.OrderID +
                ” : “ + o.Freight +
                ” : “ + ol.ProductID);
        }
    }
}

public void selects_customers_by_id_for_advanced_users()
{
    var cust = NorthwindLangQuery
        .BeginQuery()
        .Customers()
        .EndCustomerQuery()
        .As_Unique_Where((cLeft, cRight) =>
            cLeft.CustomerID == cRight.CustomerID);

    foreach (var c in cust)
    {
        Console.WriteLine(c.CompanyName);
    }
}

You can download the entire source code from the repository here.

Share/Save/Bookmark

No comments yet. Be the first.

Leave a reply

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