Thursday, December 10, 2009

NHibernate: Better Criteria API Usage with T4 Templates

UPDATE 12/22/09 - The mapping file and T4 template code have been updated to reflect a more typical NHibernate mapping file which contains the XML namespace attribute of the <hibernate-mapping> element.


In this post, we are going to take a look at NHibernate's Criteria API and see how T4 templates, which are built into Visual Studio 2008, can help create clean queries and reduce your application's technical debt.

Before we begin

NHibernate is a very powerful object-relational mapping (ORM) library that was ported from the Hibernate library for Java EE to .NET. If you aren't familiar with the core principles of NHibernate or any ORM, I suggest you read more about it here.

Also, it doesn't hurt to have a little background on what a T4 template is. A simple Google search for "Visual Studio T4 Templates" can deliver you plenty of information about the technology.

The problem

NHibernate provides a set of APIs for querying data and getting results back as fully hydrated entities. This is called the Criteria API (or sometimes referred to as ICriteria since this is the interface type that is usually being worked with).

Criteria was written with the mindset that you are invoking methods and passing string representations of your entity's associations and properties (not database columns or tables). These are then used under-the-hood to create SQL statements based on your entity's mapping file.

A simple demonstration

To better explain the issue, let's create a table called Employee as defined below.



A simple SQL Statement to retrieve all the users who were named John and 65 or older would be
SELECT EmployeeID, Name, Age, Title 
FROM Employee 
WHERE Age >= 65 AND Name LIKE 'John%'
Now lets write this using NHibernate's Criteria API that will give us back the same results as hydrated Employee entities.
//get NHibernate session instance somehow
ISession session = this.GetNHibernateSession();
ICriteria criteria = session.CreateCriteria<Employee>();

criteria.Add(Expression.Ge("Age", 65));
criteria.Add(Expression.Like("Name", "John", MatchMode.End));
IList<Employee> employeesAtRetirementAge = criteria.List<Employee>();
Did you see how we had to specify our Age and Name property as a string? Yuck!

At this point, you may be thinking "what is the big deal?". Well, often with simple examples its difficult to fully appreciate the impact on maintenance but as your application scales out to several entities, many queries, and multiple design changes later you will find that keeping all that in sync can be cumbersome. Even further, having multiple hardcoded strings can introduce typos that just stand in your way of getting the job done.

So lets say you've coded this but a few weeks later when code reviews are being done, someone on your team tells you that "Name" is too ambigous and should be changed to "FullName" for clarity (yes this scenario is silly but lets go with it).

Wouldn't it be nice to not have to go find all the places you've hard coded "Name" and fix them or worry about making typos?

The solution

We are going to solve this by writing a T4 template that will read through our Employee.hbm.xml file and generate a static metadata class for us to use in our Criteria API.

Let's start by creating a new T4 template next to our Employee.cs and Employee.hbm.xml mapping file. Visual Studio doesn't show templates listed in the New Item dialog so I choose "Text File" and then rename it from a .txt to .tt extension.



After it has been added, you will see that a "code generation" file of the same name will be created along with your .tt file. This additional file will contain the code we are going to generate and include in our project.



Before we can write our template code, we need to know exactly what our mapping file looks like. Here is our Employee.hbm.xml. It basically defines our properties and what columns they map to.

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
    <class name="NHibernateSample.Data.Employee" table="Employee">

        <!-- Primary Key Mapping -->
        <id name="ID" column="EmployeeID" unsaved-value="0">
            <generator class="native" />
        </id>

        <!-- Column Mapping -->
        <property name="Name" column="Name" />
        <property name="Age" column="Age" />
        <property name="Title" column="Title" />
    </class>
</hibernate-mapping>
Now to the good stuff. Here is our definition for our T4 Template which leverages Linq to XML. At the heart, there is one method called ProcessMappingFiles that loops through each mapping file in the specified directory. It then looks for any property definitions and builds out a static string for it.

<#@ template language="c#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>
using System;

<#= ProcessMappingFiles() #>

<#+ 
    private static string ProcessMappingFiles()
    {
        StringBuilder builder = new StringBuilder();
        DirectoryInfo currentPath = new DirectoryInfo(@"C:\NhibernateSample.Data\");
        
        builder.AppendLine("namespace NHibernateSample.Data.Meta");
        builder.AppendLine("{");
        
        foreach (FileInfo file in currentPath.GetFiles("*.hbm.xml"))
        {
            StreamReader reader = new StreamReader(file.FullName);
            XElement xmlReader = XElement.Parse(reader.ReadToEnd());
            XNamespace xmlns = "urn:nhibernate-mapping-2.2";
            
            XElement classElement = xmlReader.Element(xmlns + "class");
            string fullName = classElement.Attribute("name").Value;
            string className = fullName.Substring(fullName.LastIndexOf('.') + 1);
            
            builder.AppendLine(string.Format("\tpublic static class {0}Meta", className));
            builder.AppendLine("\t{");
            
            foreach(XElement element in classElement.Elements(xmlns + "property"))
            {
                builder.AppendLine(string.Format("\t\tpublic static readonly string {0} = \"{0}\"", element.Attribute("name").Value));
            }
            
            builder.AppendLine("\t}");
            builder.AppendLine();
        }    
        
        builder.AppendLine("}");
        return builder.ToString();
    }
#>

Final usage

So you can see in our code snippet below that after we have our template, we can replace our hardcoded strings with a more strongly typed property definition.

//get NHibernate session instance somehow
ISession session = this.GetNHibernateSession();
ICriteria criteria = session.CreateCriteria<Employee>();

criteria.Add(Expression.Ge(EmployeeMeta.Age, 65);
criteria.Add(Expression.Like(EmployeeMeta.Name, "John", MatchMode.End);
IList<Employee> employeesAtRetirementAge = criteria.List<Employee>();
Obviously there is much more to mapping files besides just properties. You can easily extend out the template to parse your associations for usage in Criteria joins or aliases.

That is all there is to it. I encourage NHibernate users that rely on mapping files and ICriteria to give this a shot. I presently use this on my current project and it has saved a ton of time as well as made my code less error prone.

1 comment:

  1. Thanks for the update. Trying to get it to work with my files was driving me nuts (though I should have noticed the issue).

    ReplyDelete