Monday 9 March 2009

Supporting multiple NHibernate versions in Fluent NHibernate semantic model

My Fluent NHibernate semantic model rewrite is designed to support multiple versions of NHibernate simultaneously. At the moment, I support NH 2.0.1GA and 2.1.0.1001 (this was a trunk snapshot). My goal is to support the latest official release and to keep as up to date with the trunk as possible; time will tell how well this pans out.

The semantic model rewrite makes heavy use of classes that are generated from the NH mapping schema. These classes are all prefixed with Hbm, so I am in the habit of calling them “Hbm*” classes. Here is some example code that builds a HbmKey representation of a KeyMapping:

public override void ProcessKey(KeyMapping keyMapping)
{
    _hbm = new HbmKey();

    if (keyMapping.Attributes.IsSpecified(x => x.Column))
        _hbm.column1 = keyMapping.Column;

    if (keyMapping.Attributes.IsSpecified(x => x.ForeignKey))
        _hbm.foreignkey = keyMapping.ForeignKey;

    if (keyMapping.Attributes.IsSpecified(x => x.PropertyReference))
        _hbm.propertyref = keyMapping.PropertyReference;

    if (keyMapping.Attributes.IsSpecified(x => x.CascadeOnDelete))
        _hbm.SetCascadeOnDelete(keyMapping.CascadeOnDelete);
}

Because the NH mapping schema changes from version to version, the public interface for these classes also changes from version to version. See that “column1” property? Its entirely possible it was called “column” in an earlier version of NH. Obviously, if it were to change back to “column” tomorrow, this code would no longer compile. Given this, how can Fluent NH hope to simultaneously support multiple NH versions? The answer is to put any version specific code in a separate assembly.

Today I will fix a NH2.1 incompatibility that I introduced a few days ago when adding the ability to specify the column name for a property. Here is the code I wrote while working against NH2.0.1:

public override void ProcessProperty(PropertyMapping propertyMapping)
{
    _hbm = new HbmProperty();
    _hbm.name = propertyMapping.Name;

    if(propertyMapping.Attributes.IsSpecified(x => x.IsNotNullable))
        _hbm.SetNotNull(propertyMapping.IsNotNullable);

    if (propertyMapping.Attributes.IsSpecified(x => x.Length))
        _hbm.length = propertyMapping.Length.ToString();

    if (propertyMapping.Attributes.IsSpecified(x => x.ColumnName))
        _hbm.column1 = propertyMapping.ColumnName;
}

The very last line is the one to watch here. You can see its setting the “column1” property. This worked fine against NH2.0.1 but when I ran my rake script to switch over to NH2.1 today, this code no longer compiled - the property is now called “column”. As I mentioned above, my strategy for dealing with this problem is to use version specific assemblies. Here is the Fluent NH solution:

image

The two “Versioning” projects are where the version specific code goes – they each reference their own version of NHibernate. Each project contains the same set of extensions methods, each method implemented slightly differently depending on the versioning requirements. The two projects share the same namespace and output an assembly with the same name (FluentNHibernate.Versioning) and to the same location. Neither project is included in the default build configuration, so they have to be built explicitly. The act of building one of these versioning projects will make it “active” because the assembly is output to the same location that the FluentNHibernate project is referencing.

With the background out of the way, lets fix the problem with with “column1” property on HbmProperty. First I open the Versioning.NH20 project, find the HbmPropertyExtensions and add an extension method:

public static class HbmPropertyExtensions
{
    ...
public static void SetColumn(this HbmProperty hbmProperty, string column) { hbmProperty.column1 = column; } }

I then modify the code to call the extension method:

if (propertyMapping.Attributes.IsSpecified(x => x.ColumnName))
    _hbm.SetColumn(propertyMapping.ColumnName);

I compile the Versioning.NH20 project to make sure its active and run my tests. I already have a test that is verifying this implementation – here it is:

[Test]
public void Should_write_the_attributes()
{
    var testHelper = new HbmTestHelper<PropertyMapping>();
    testHelper.Check(x => x.Name, "test").MapsToAttribute("name");
    testHelper.Check(x => x.Length, 50).MapsToAttribute("length");
    testHelper.Check(x => x.IsNotNullable, true).MapsToAttribute("not-null");
    testHelper.Check(x => x.ColumnName, "thecolumn").MapsToAttribute("column"); <--- HERE
    var writer = new HbmPropertyWriter();
    testHelper.VerifyAll(writer);
}

Because this test verifies that a certain property is written to the correct xml attribute, it works for both versions of NH. Regardless of what the property is called on the Hbm class, its written to the “column” attribute.

The tests all pass, so I switch to NH2.1 using my rake script and move to the Versioning.NH21 project. Again, I add an extension method:

public static class HbmPropertyExtensions
{
    ...
public static void SetColumn(this HbmProperty hbmProperty, string column) { hbmProperty.column = column; } }

This looks almost the same as the code above, but notice that the “1” is gone. I compile the Versioning.NH21 project to make sure its active and run the tests. They all pass!

So far, all the versioning issues I’ve ran into were easily fixed like the one above. Lets hope they are all this simple!

 

4 comments:

  1. Take care with it because we don't begin the full support of Hbm* and so far NH are mixing Hbm* with pure XML parse.

    ReplyDelete
  2. Hi Fabio,

    Thats ok, I write all the Hbm* to XML and feed that to NH. Hopefully NH will eventually move to using Hbm* and then we can skip the XML step altogether.

    ReplyDelete
  3. Your test is guilty of "multiple asserts per test". From my point of view ;)

    ReplyDelete
  4. Hi Weex, thanks for the comment. I think your statement is true from pretty much -any- point of view. But I expect the list of attributes to grow long for many of the types, and I don't feel comfortable defining a test for each one. Perhaps one day I will start to feel the pain and change my mind, but at the moment this is lower friction and feels fine.

    ReplyDelete