3

Chapter 3: LINQ as DSL

Line of business (LOB) applications are mainly driven by CRUD operations on the domain data.  There are various data storage types and data accessing techniques available.  Each one is designed to tolerate particular architecture principles.  Every architecture principle is created to handle some operational constraints.  For example, we use SQL to manipulate the data in the relational structure.  The main constraint resolved by relational structures are consistency and data integrity.  This programming style is different from C#.

However, instead of switching back and forth between various programming styles, a unified programming model for data access will improve the productivity and reduce the learning curve.  In particular, there are some domain specific programming models introduced in some industries.  GELLO is one such specification in the healthcare industry by HL7.  The objective of this language is to get records from an “implementation neutral” domain model called vMR (virtual Medical Record).

In this chapter, I use the GELLO specification to blend with LINQ programming model so that programmers can enjoy the LINQ flavor with GELLO toppings for writing healthcare software applications.  The reason I mix these two is that GELLO is technology neutral and has a varied audience and LINQ is for general data access programming model for developers in the healthcare domain.

The “HealthPoint” application has exposed a set of “patient medical records” as a RESTful service.  The “HealthPoint” customers use this service to build their line-of-business applications.  Instead of accessing this service, they prefer a stable programming interface based on vMR.  In addition to this, developers of their line-of-business applications want GELLO based programming style, because most of them are having good knowledge in healthcare domain and if we give GELLO like programming style, the productivity would be higher.

In order to blend GELLO on LINQ, we cannot provide API based on LINQ to Objects or LINQ to Entities, instead, a custom LINQ extension.  Let us call this solution as “GELINQ” (GEllo LINQ).

About LINQ

LINQ is a uniform programming model in .NET for accessing and managing data similar to SQL in relational database.  The data could be in-memory objects, relational database, XML, even flat files and even from web services.  LINQ is strongly built on top of following .NET 3.5 features:

  • Anonymous method
  • Enumerators and yield
  • Local type inference
  • Lambda expression
  • Extension methods
  • Anonymous types

It provides standard language syntax for example Select, From, Where, Group, Into, OrderBy, Join, Let, etc.  The data integrity, constraints and data access pattern vary between data source.  Due to this, different flavors of LINQ are available.  For example, the below code fetches details of a patient with id “PAT1234”

from p in patients
where p.Id == “PAT1234”
select p

“patients” is the data source.  The above query works perfectly with relational database or in-memory objects.  Let us take another example.  This query retrieves medications details of a patient with id “PAT1234” who has “dowager hump” problem.

var medications =
    from p in patients
    where p.Problem == "Dowager Hump" and p.Id == “PATID1212”
    select p.Medications

If you apply the above query in LINQ to SQL, it expects a database table “Patient” with columns “Problem” and “Id”.  Now, assume the database actually has a Problem table, which refers to Patient table to represent that “a patient has one or more problems”.  Your customer does not want to get into the relational database details to write GELINQ queries, instead they will be happy with the above query which makes logical sense to their real-world business model.  This query will annoy them, when you support a “JOIN”, which is purely set theory in relational databases.

On the other hand, let us assume that GELINQ uses a custom LINQ provider, which understands the above query and generates the actual data store query to access the health record, as shown in the figure below.

The best part of LINQ is that you can provide your syntactic sugar to the LINQ parser to understand your own version of LINQ query.  For GELINQ, a LINQ query is executed against the vMR methods that in turn invoke the RESTful GelinqService as shown in figure below.

Extending LINQ

LINQ provides excellent infrastructure for extending its programming style to provide custom LINQ implementation for a data store.  Before entering into the infrastructure topic, let us see the expression tree, a powerful mechanism in .NET that is the backbone of both LINQ and DLR (Dynamic Language Runtime).

Inspired by functional programming languages, C# 3.0 introduces Lambda Expression, which provides a concise syntax to define anonymous method.  For example the below code define an anonymous method for (a + b)2 based on the formula a2 + b2 + 2ab.

class A...
    delegate int Square(int a, int b);
    void Main()...
        Square s = delegate(int a, int b) { return a * a + b * b + 2 * a * b; };
        Console.WriteLine(s(2, 2));  // writes 16

Instead, a lambda expression of the Square definition would be

Square x = (a, b) => a * a + b * b + 2 * a * b;
Console.WriteLine(x (2, 2)); // writes 16

Or, more precisely without delegate declaration,

Func<int, int, int> x = (a, b) => a * a + b * b + 2 * a * b;
Console.WriteLine(x(2, 2)); // writes 16

In the functional programming world, the function “x” is described as int → int → int, which means that a function takes an integer parameter, returns another integer, which is again an input to another function and that method in turn returns an integer.

Lambda expression also have the ability to defer code generation by creating an expression tree.  The expression tree sees the lambda expression as tree with nodes as operands and operators.  See the below figure, an expression tree for the above expression (a2 + b2 + 2ab).

Operands are not just local variables; it can be method argument, C# statement, method call and so on.  Hence, an expression tree can accommodate any valid C# syntax.  The below code shows how to create an expression tree for a lambda expression:

 

The advantage with expression tree is that you can scan and take a walk in the tree nodes; take appropriate actions based on the types of nodes.  This option enables us to provide custom LINQ.  As long as, you have lambda expression as expression tree, you can control the code.  Once you want the runtime to evaluation it, you can simply call the Compile() method as shown below:

Console.WriteLine(x.Compile()(3,2)); // prints 25

Every node in an expression tree is an expression and represented by System.Linq.Expressions.Expression.  In the above example, “+” and “*” are binary expression, “a” and “b” are parameter expressions, and “2” is a constant expression.  They have concrete expression types derived from Expression.  Some of the concrete expression types are

  • BinaryExpression
  • MethodCallExpression
  • MemberExpression
  • ConstantExpression

Expression type has a property called “NodeType” that further classifies what exactly an expression contains, for example Add, Multiply, LessThan, GreaterThanOrEqual are some of the node types for BinaryExpression type.

Let us now see the infrastructure for extending LINQ for your custom needs. The figure below depicts the conceptual representation of the LINQ infrastructure.

LINQ Classes

Once you define the LINQ query syntax, any valid LINQ queries are parsed and converted as expression tree.  It is then given to your custom query provider, which implements System.Linq.IQueryProvider interface, by System.Linq.IQueryable.  The expression tree is then given to your custom expression visitor, which derives from System.Linq.Expressions.ExpressionVisitor to visit through the appropriate nodes and invoke the methods that are representatives for your action when parsing your custom query.  After the tree visiting, the result is evaluated as IEnumerable, which represents a collection of data from the underlying data store.

The IQueryProvider has following members:

interface IQueryProvider...
   IQueryable CreateQuery(Expression expression)
   IQueryable<TElement> CreateQuery<TElement>(Expression expression)
   object Execute(Expression expression)
   TResult Execute<TResult>(Expression expression)

CreateQuery() method  is used to construct an IQueryable object that can evaluate the query represented by a specified expression tree “expression”.  The Execute() method executes the LINQ query.

Let us see the ExpressionVisitor class:

class ExpressionVisitor...
   Expression Visit(Expression node)
   Expression VisitBinary(BinaryExpression node)
   Expression VisitMethodCall(MethodCallExpression node)
   Expression VisitMember(MemberExpression node)
   Expression VisitMemberAccess(MemberAccessExpression node)
   Expression VisitConstant(ConstantExpression node)
   ....

This class provides methods to traverse the nodes and allows us to do necessary actions.  Whatever node you want to apply your actions on, you need to implement your custom expression visitor that is derived from this class and override the appropriate VisitXXX methods.

As I mentioned earlier in this section, the result of the execution is a collection of data items from a data store that implement IQueryable interface, which is an extension of IEnumerable.  The reason for this extension is to play with the expression tree.  It means that IQueryable has introduced to play around with Expression object instead of handling the regular .NET objects as IEnumerable takes.  Let us see the members.

interface IQueryable<out T> : IEnumerable<T> ...
   Type ElementType {get;}
   Expression Expression {get;}
   IQueryProvider Provider {get;}

You can consider IQueryable as a wrapper around Expression.  The wrapper has a Provider property to specify a LINQ provider, which interprets and executes the expression tree.  Another property ElementType returns the type of elements that are returned when the expression tree associated with the instance of IQueryable is executed.

Let us take a simple example, as expressed below to understand the role of IQueryable in LINQ.

string[] culinaryFruits =
{
       "Apple", "Cherry", "Plum", "Dates", "Orange", "Lemon"
};

IQueryable<string> queryFruits = culinaryFruits.AsQueryable();
Console.WriteLine("Provider Type: {0}", queryFruits.Provider.GetType());
Console.WriteLine("Element Type: {0}", queryFruits.ElementType);
Console.WriteLine("Expression: {0}", queryFruits.Expression);

A string array (which is actually IEnumerable<string>) has converted into IQueryable<string> by using AsQueryable() extension method of IEnumerable<T>.  Then the provider, element type and the actual expression are displayed.  The output would be

Provider Type: System.Linq.EnumerableQuery`1[System.String]

Element Type: System.String

Expression: System.String[]

EnumerableQuery`1 has default LINQ provider.  Now, let us start writing the custom LINQ provider for the GELLO.

Designing the Specification

The RESTful GelloService has the following resources and parameters:

Let me explain the above service using some use cases.  Assume that a medical officer wants to get the following details from a particular healthcare organization:

  • Case 1: List out patients with age above 60 years
  • Case 2: List out patients who have suffered from “Wheezing” 6 months ago
  • Case 3: List out patients who have taken the drug “Salmeterol”
  • Case 4: List out the details of  the problem “Wheezing” for a patient with id “PAT1256”
  • Case 5: List out the details of the “Salmeterol” medication for a patient with id “PAT1234”

Let us define the format of GELINQ specification by the above use cases.

To Get Patient Lists with Age condition:

from patient in GelinqService<Patient>
where patient.Age > 60
select patient

To Get patient lists those taken particular drug:

from p in GelinqService<Patient>
where p.Medications.Has("Salmeterol")
select p;

Presence of particular problem during particular period:

from pat in GelinqService<Patient>
where pat.Problems.Has("Wheezing") && pat.Problems.IsStarted(1, PeriodOn.YearAgo)
select pat

Get patients medication detail:

from p in GelinqService<Patient>
where p.Medications.Has("Salmeterol") && p.PatientId == "PAT1256"
select p.Medications

Get patients problem details:

from p in GelinqService<Patient>
where p.Problems.Has("Wheezing") && p.PatientId == " PAT1234"
select p.Problems

A RESTful service is the actual interface to get the above medical records.  The objective of GELINQ is to enable developers to write above specified queries based on vMR.  Internally, GELINQ should perform the following:

  • Parse the queries,
  • Find the appropriate REST resource, its parameters and HTTP method
  • Convert the data in vMR format into appropriate RESTful service interface data format
  • Invoke the service operation and gets the data as vMR

I use WCF4.0 WebHttpBinding for the RESTful service GelloService.  The medical records are stored in an SQL Server database and I have used ADO.NET Entity Framework 4.0 as ORM.  I gloss over the details of that implementation and concentrate only on GELINQ.  You can go through the source code for further details.  The structure of the project is as illustrated:

The EnterpriseLayer contains data contracts and common data types definitions for the service.  ConsumerLayer contains implementation neutral MedicalRecord (vMR) and one of its implementations on top of the GelloService.  In addition to these, it contains GELLO implementation.  I have used Managed Extensibility Framework (MEF) to specify the concrete implementation for the abstract vMR object model.  The implementation is specified as vMR by the MEF code given below:

private CompositionContainer _container;
public Patient Patient;

public void Init()
{
   Patient = new Patient();

   //An aggregate catalog that combines multiple catalogs
   var catalog = new AggregateCatalog();

   //Adds all the parts found in the same assembly as this class
   catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));

   //Create the CompositionContainer with the parts in the catalog
   _container = new CompositionContainer(catalog);

   //Fill the imports of this object
   try
   {
        this._container.ComposeParts(Patient);
   }
   catch (CompositionException compositionException)
   {
        Console.WriteLine(compositionException.ToString());
   }
}

Note: The Patient class has all the business methods; hence I use this as a composable part.  Since the concept behind MEF is beyond the scope of this book, I have skipped the explanation of the above source code.

The LINQ version of GELLO uses the typical LINQ methods From, Where, Select and, the logical and relational operators.  In addition to these, the following methods are required:

  • Has() on IEnumerable<Medication> to filter medications with particular drug generic name
  • Has() on IEnumerable<Problem> to filter problems with particular problem name
  • IsStarted() on IEnumerable<Problem> to provide filter criteria for Case 2

Parsing: Custom LINQ Implementation

The GELINQ implementation model is depicted below:

Note: The GelinqService<T> is an implementation of IQueryable<T>.  It is unrelated to GelloService RESTful service.  Do not confuse.

Implementing IQueryProvider

Let us start implementing IQueryProvider as shown below (code 3.1).

public class GelinqQueryProvider : IQueryProvider
{    

    public IQueryable CreateQuery(Expression expression)
    {
        Type elementType = TypeSystem.GetElementType(expression.Type);
        try
        {
            return (IQueryable)Activator.CreateInstance(typeof(GelinqService<>).
                MakeGenericType(elementType), new object[] { this, expression });
        }
        catch (System.Reflection.TargetInvocationException tie)
        {
            throw tie.InnerException;
        }
    }

    public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
    {
        return new GelinqService<TResult>(this, expression);
    }

    public object Execute(Expression expression)
    {

        GelinqParserHelper helper = new GelinqServiceContext().Translate(expression, false)
             as GelinqParserHelper;
        return GenerateResult(helper);
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return (TResult)Execute(expression);
    }

    // other implementation
}

GelinqQueryProvider implements IQueryProvider interface.  It implements both generic and non-generic versions of CreateQuery() and Execute() methods.

The generic version of CreateQuery() passes the expression tree to GelinqService<T> which is the implementation of  IQueryable<T> interface.  The non-generic version of CreateQuery() simply instantiates GelinqService with the instance of the Expression’s root element type.

The non-generic version of Execute() contains the actual tree walking implementation.  It uses GelinqServiceContext, which is the concrete implementation of ExpressionVisitor to enable tree walking.  Based on the tree walking, set of intermediate values are determined and stored in GelinqParserHelper instance.  In the action phase, the GelinqParserHelper is available to GenerateResult() method of GelinqQueryProvider to invoke appropriate RESTful resource to fetch health records.

Implementing IQueryable

Next, let us implement IQueryable<T> as shown below (code 3.2).

public class GelinqService<TRecord> : IQueryable<TRecord>
{
    public IQueryProvider Provider { get; private set; }
    public Expression Expression { get; private set; }

    public GelinqService()
    {
        Provider = new GelinqQueryProvider();
        Expression = Expression.Constant(this);
    }

    public GelinqService(GelinqQueryProvider provider, Expression expression)
    {
        if (provider == null)
        {
            throw new ArgumentNullException("provider");
        }

        if (expression == null)
        {
            throw new ArgumentNullException("expression");
        }

        if (!typeof(IQueryable<TRecord>).IsAssignableFrom(expression.Type))
        {
            throw new ArgumentOutOfRangeException("expression");
        }

        Provider = provider;
        Expression = expression;
    }

    public Type ElementType
    {
        get { return typeof(TRecord); }
    }

    #region Enumerators
    public IEnumerator<TRecord> GetEnumerator()
    {
        return (Provider.Execute<IEnumerable<TRecord>>(Expression)).GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return (Provider.Execute<System.Collections.IEnumerable>(Expression)).GetEnumerator();
    }
    #endregion
}

The Provider is an instance of GelinqQueryProvider.  By default, a constant expression is assigned to Expression property.  The generic type is returned as ElementType.  The data source for which queries are executed should be specified in IEnumerable’s generic and non-generic GetEnumerator() method.  This implementation uses its provider’s Execute method and passes the expression tree to it.  GelinqService receives provider and expression tree instances through its non-default constructor.

Customizing ExpressionVisitor

Let us customize the expression visitor.  I have defined GelinqServiceContext class, which extends ExpressionVisotor. (code 3.3)

public class GelinqServiceContext : ExpressionVisitor
{     
    GelinqParserHelper _handler;

    internal GelinqServiceContext()
    {                 
    }

    internal object Translate(Expression e, bool IsEnumerable)
    {
        _handler = new GelinqParserHelper();
        this.Visit(e);
        return _handler;
    }

    //Customized VisitXXX() implementation
    //helper methods
}

This class depends on GelinqParserHelper to store some intermediate understanding of the query.  The Translate() method is the starting point for visiting the expression tree.  During the visit, the customized VisitXXX() methods are invoked and intermediate values are assigned to GelinqParserHelper.  The GelinqParserHelper class has following members:

internal class GelinqParserHelper
{
   public string TargetMedicalRecord;
   public string CurrentMethodContext;
   public string ResultMedicalRecord;

   public PatientSearchFilter PatientSearchFilter;
   public bool PatientSearchByMedication;
   public string MedicationGenericName;
   public ProblemSearchFilter ProblemSearchFilter;
}

Whenever a query is executed, we need to invoke appropriate method in the classes of vMR. It could be “Patient”, “Problem” or “Medication” (In this book, only Patient class has all the methods.  However, this would not be the case in practice.)  The expression visitor uses TargetMedicalRecord to store the target medical record type.  During the visiting, the visitor may travel to one or more methods like Where, Has and IsStarted.  Some action needs to be taken whenever these methods are encountered.  The CurrentMethodContext is used to specify the currently visited LINQ method.  The PatientSearchFilter is used to convey the filter criteria for patient search in the Where method of LINQ.  PatientSearchByMedication flag is used to specify whether the patient search is based on medication or other criteria.  If the search is based on medication, then MedicationGenericName contains the generic name of the drug.  The ProblemSearchFilter contains filter criteria for patient search based on Problems.  The precedence of filter criteria is patient search filter, medication filter then problem search filter.

Visiting the Tree

Based on the various GELINQ queries in “Designing the Specifications” section, the following nodes need to be customized:

  • MethodCallExpression – for Where, Has, IsStarted methods
  • MemberExpression – for p.Medication, p.Problem member visit
  • ConstantExpression – for constant values visit like 60
  • BinaryExpression – for “and, <, >, <=, >=, ==” expressions

Before writing the visit implementation, you may be surprised with the methods “Where”, “Has” and “IsStarted” since they are neither the part of vMR nor the LINQ methods.  In order to provide a query syntax like the case below

from p in GelinqService<Patient>
where p.Medications.Has("medication")
select p;

I define these methods as extension methods on the appropriate classes as shown below (code 3.5).

public static bool Has(this IEnumerable<Medication> source,
    string genericName)
{
    return true;
}

public static bool Has(this IEnumerable<Problem> source,
    string problemName)
{
    return true;
}

public static bool IsStarted(this IEnumerable<Problem> source,
    int agoValue, string agoSymbol)
{
    return true;
}

“Has” methods defined on IEnumerable<Medication> and IEnumerable<Problem>, and “IsStarted” method defined on IEnumerable<Problem>.  You might be surprised again that these methods have not implementation apart from returning the value “true”!  In truth, these methods are never invoked; instead, they are used only on GELINQ queries and parsed during the tree walking.  I have defined these methods in ExtensionMethods class.

Let us implement MethodCallExpression visit as shown below (code 3.6).

protected override Expression VisitMethodCall(MethodCallExpression m)
{
    if (m.Method.DeclaringType == typeof(Queryable))
    {
        switch (m.Method.Name)
        {
            case "Where":
                _handler.CurrentMethodContext = "Where";
                //m.Arguments[0]: GelinqService<T>
                this.Visit(m.Arguments[0]);
                //m.Arguments[1]: where predicate
                LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
                this.Visit(lambda.Body);
                return m;
        }
    }
    else if (m.Method.DeclaringType.FullName.Equals("Udooz.Net.Enterprise.Gelinq.ExtensionMethods"))
    {
        if(m.Method.Name.Equals("Has"))
        {
                _handler.CurrentMethodContext = "Has";
                this.Visit(m.Arguments[0]);
                this.Visit(m.Arguments[1]);
                return m;
        }
        else if (m.Method.Name.Equals("IsStarted"))
        {
            _handler.CurrentMethodContext = "IsStarted";
            this.Visit(m.Arguments[0]);
            this.Visit(m.Arguments[1]);
            this.Visit(m.Arguments[2]);
            return m;
        }
    }

    throw new NotSupportedException(string.Format("The method '{0}' is not supported",
        m.Method.Name));
}

GELINQ query has two different types of methods, “Where” method, which is part of LINQ (defined in Queryable class), and “Has” and “IsStarted” are part of extension methods (defined in ExtensionMethods class).  The Method.DeclaringType property helps us to identify the class, which in turn helps us to narrow down the method name identification.

To handle “Where” method, you should know the definition which is

public static IEnumerable<TSource> Where<TSource>(
   this IEnumerable<TSource> source,
   Func<TSource, Boolean> predicate)

“Where” is the extension method of IEnumerable<T>, which requires the data collection (source) and the predicate as input parameters.  The implementation logic of “Where” should apply the predicate on the data collection and returns the matched records.  GelinqService<T> is the data source for GELINQ.

Once the method name identified, these arguments are extracted from this node and given to the expression visitor for further visit.  Similar to this, “Has” and “IsStarted” methods’ parameters are also further visited.

Since, “Has” method is declared on both IEnumerable<Problem> and IEnumerable<Medication>, we need to know whether the patient search is by medication or problem.  The following visit helps to determine this (code 3.7).

protected override Expression VisitMember(MemberExpression mnode)
{
    if (_handler.CurrentMethodContext.Equals("Has"))
    {
        if (mnode.Type.FullName.Contains("List`1"))
        {
            Regex regex = new Regex(@"Udooz\.Net\.Gelinq\.MedicalRecord\.[a-z|A-Z]{1,}",
            RegexOptions.IgnoreCase);
            Match match = regex.Match(mnode.Type.FullName);
            if (match.Value.Equals("Udooz.Net.Gelinq.MedicalRecord.Medication"))
            {
                _handler.PatientSearchByMedication = true;
            }
            else if (match.Value.Equals("Udooz.Net.Gelinq.MedicalRecord.Problem"))
            {
                if (_handler.ProblemSearchFilter == null)
                    _handler.ProblemSearchFilter = new ProblemSearchFilter();
            }
        }
    }
    return mnode;
}

The MemberExpression specifies whether the “Has()” method is invoked from p.Medications or p.Problems.  Based on this, PatientSearchByMedication flag is set as true.

When you take the below query

from patient in GelinqService<Patient>
where patient.Age > 60
select patient

So far, we have identified the “where” method.  The source parameter is GelinqService<Patient> and predicate parameter is patient.Age > 60.  The next thing is to visit the body of the lambda expression “predicate” (code 3.8) which is  “patient.Age > 60”.  The body of a lambda expression is BinaryExpression.

protected override Expression VisitBinary(BinaryExpression bnode)
{
    switch (bnode.NodeType)
    {
        case ExpressionType.And:
        case ExpressionType.AndAlso:
            this.Visit(bnode.Left);
            this.Visit(bnode.Right);
            return bnode;

        case ExpressionType.Equal:
        case ExpressionType.LessThanOrEqual:
        case ExpressionType.GreaterThanOrEqual:
        case ExpressionType.GreaterThan:
        case ExpressionType.LessThan:
            return VisitBinaryComparison(bnode);
        default:
            throw new NotSupportedException(string.Format(
                "The binary operator {0} is not supported", bnode.NodeType));
    }
}

In LINQ query, the conditional expression “and” has left side and right side conditional or relation expressions.  For example, in “where pat.Problems.Has(“Wheezing”) && pat.Problems.IsStarted(1, PeriodOn.YearAgo)” part of a GELINQ query, pat.Problems.Has(“Wheezing”) is on the left side and pat.Problems.IsStarted(1, PeriodOn.YearAgo) on the right side of the “and” condition.

The relational expression has a relational operator with left and right side would be member expression like p.Age or constants expression for example “60”.  The is handled by a helper method VisitBinaryComparison()as shown below (code 3.9).

private Expression VisitBinaryComparison(BinaryExpression bnode)
{
    ConstantExpression constant =
        (bnode.Left as ConstantExpression ?? bnode.Right as ConstantExpression);
    MemberExpression memberAccess =
        (bnode.Left as MemberExpression ?? bnode.Right as MemberExpression);

    if (memberAccess == null || constant == null)
    {
        throw new NotSupportedException(string.Format("One of the operand not supported for operator {0}",
            bnode.NodeType));
    }

    if (constant.Value == null)
    {
        throw new NotSupportedException(string.Format("NULL is not supported for {0}",
            bnode.ToString()));
    }

    TypeCode constantTypeCode = Type.GetTypeCode(constant.Value.GetType());
    if (constantTypeCode != TypeCode.Int32 && constantTypeCode != TypeCode.String)
        throw new NotSupportedException(string.Format("Constant {0} is of an unsupported type {1}",
            constant.ToString(), constant.Value.GetType().Name));
    TranslateStandardComparison(bnode.NodeType, constant, memberAccess);
    return bnode;
}

The method starts with identifying the exact expression type of the given binary expression, which is either ConstantExpression or MemberExpression.  GELINQ throws exception when it finds constant expression other than integer or string.  Then, another helper method “TranslateStandardComparison” evaluates the expression as shown below (figure 3.10).

private void TranslateStandardComparison(ExpressionType nodeType, ConstantExpression constant,
            MemberExpression memberAccess)
{
    Type t = Assembly.Load("MedicalRecord").GetType(_handler.TargetMedicalRecord);
    string propertyName =
        (from field in t.GetProperties()
         where field.Name == memberAccess.Member.Name
         select field.Name).FirstOrDefault();

    if (_handler.TargetMedicalRecord.Contains("Patient"))
    {
        if(_handler.PatientSearchFilter == null)
            _handler.PatientSearchFilter = new PatientSearchFilter();
        switch (propertyName)
        {
            case "Age":
                if(_handler.PatientSearchFilter.AgeRange == null)
                    _handler.PatientSearchFilter.AgeRange = new PatientAgeRange();
                if (nodeType == ExpressionType.LessThan || nodeType == ExpressionType.LessThanOrEqual)
                    _handler.PatientSearchFilter.AgeRange.High = (int) constant.Value;
                else if (nodeType == ExpressionType.GreaterThan || nodeType == ExpressionType.GreaterThanOrEqual)
                    _handler.PatientSearchFilter.AgeRange.Low = (int)constant.Value;
                break;
            case "Id":
                if(nodeType == ExpressionType.Equal)
                    _handler.PatientSearchFilter.PatientId = (string)constant.Value;
                break;
            case "Name":                        
                if(nodeType == ExpressionType.Equal)
                    _handler.PatientSearchFilter.PatientName = (string)constant.Value;
                break;
        }
    }
}

The property name is evaluated by reflecting the MedicalRecord assembly (vMR).  In this exercise, I handle only Patient class.   Based on the property, the appropriate properties of GelinqParserHelper instance “_helper” are determined.

Defining Action

Once the GelinqQueryProvider successfully make the tree visit, it has the GelinqParserHelper instance in its Execute() method.  The GenerateResult() method shown below actually processes the helper. (code 3.11)

object GenerateResult(GelinqParserHelper helper)
{
    InitPatient();

    if (helper == null)
        throw new InvalidOperationException("The given LINQ query yields no means");
    if (helper.PatientSearchFilter != null)
        return _patient.GetPatients(helper.PatientSearchFilter);
    else if (helper.PatientSearchByMedication)
        return _patient.GetPatientsTakenMedication(helper.MedicationGenericName);
    else if (helper.ProblemSearchFilter != null)
        return _patient.GetPatientsHavingProblem(helper.ProblemSearchFilter);

    return null;
}

This method calls InitPatient() method and when it finds PatientSearchFilter, vMR’s GetPatient() method in Patient class is invoked.  If PatientSearchByMedication is true, then GetPatientsTakenMedicatin() is invoked.  If ProblemSearchFilter is available, then GetPatientsHavingProblem() on is invoked.  The InitPatient() is used MEF API to get concrete implementation of MedicalRecord as shown below (code 3.12).

void InitPatient()
{
    _patient = new Patient();

    //An aggregate catalog that combines multiple catalogs
    var catalog = new AggregateCatalog();
    //Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(new AssemblyCatalog(typeof(GelinqQueryProvider).Assembly));

    //Create the CompositionContainer with the parts in the catalog
    _container = new CompositionContainer(catalog);

    //Fill the imports of this object
    try
    {
        this._container.ComposeParts(_patient);
    }
    catch (CompositionException compositionException)
    {
        throw compositionException;
    }
}

A sample of the GELINQ query would be (code 3.13)

GelinqService<Patient> patients = new GelinqService<Patient>();

var query = from p in patients
            where p.Age > 10
            select p;

var query2 = from p in patients
             where p.Problems.Has("Nausea") &&
                p.Problems.IsStarted(1, PeriodOn.YearAgo)
             select p;

foreach (Patient p in query2)
{
    Console.WriteLine(p.Name);
}

Summary

LINQ is the powerful and meta-programming platform in .NET.  By providing the custom implementation, you can abstract the underlying connecting and data accessing methodologies with the data store from the consumer of your API.  This will make them concentrate on the problem domain and stay with the unified data access discipline.

License

Domain Specific Language in .NET Copyright © 2012 by udooz@hotmail.com. All Rights Reserved.

Feedback/Errata

1 Response to Chapter 3 – LINQ as DSL

  1. Tavi Thurmond on May 23, 2013 at 5:41 pm says:

    Thank you for this body of work! I am building, have been for 4 years now, LINQ DSL Providers around RDF/OWL; it’s good to see others advancing DSL technology.

    Tavi

Leave a Reply

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