Thursday, June 30, 2011

NHibernate foreign key property, solution on model binding impedance mismatch.

There's a little problem with NHibernate if you strictly adhere to its proper domain modelling. You will end up with a class like this (note Line 6):

public class SalesDetail 
{
    public virtual SalesHeader SalesHeader { get; set; }

    public virtual int SalesDetailId { get; set; } // Primary Key
    public virtual Product ProductX { get; set; }
    public virtual int Qty { get; set; }
    public virtual decimal UnitPrice { get; set; }
    public virtual decimal Amount { get; set; }

    public virtual byte[] Version { get; set; }           
}



If some components could allow direct binding to ProductX, there won't be too much problem with clean entity modeling of NHibernate. You can not use the ProductX directly as a DataPropertyName of your DataGridView for example.


There's a solution on that though, we can take a page from Entity Framework. EF forces one to use primitive types on model's foreign keys instead of stating the fact that it is a pointer to other records; for example, this is how your EF model looks like:


public class SalesDetail 
{
    public SalesHeader SalesHeader { get; set; }

    public int SalesDetailId { get; set; }
    public int ProductId { get; set; }
    public int Qty { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Amount { get; set; }

    public byte[] Version { get; set; }
           
}

See the ProductID there? It is model-binding-ready. However, if you just quick-glance that class, it is not immediately obvious that an entity(ProductId) has a relationship with other entity; for all we know, ProductId could just be a barcode misrepresented as integer type. And notice that reaching the ProductId's other properties(say Category, or StockLevel) is not readily available from that property alone. Fear not however, as we all know that the first thing you shun after using ORM is joining tables; with an ORM, you don't need to join the classes to reach the foreign key's other properties(fields, e.g. Category, StockLevel); when you have a foreign key attribute or necessary mapping for foreign key on a model, EF automatically populates its corresponding object. Like this example:


public class SalesDetail 
{
    public SalesHeader SalesHeader { get; set; }

    public int SalesDetailId { get; set; }
    public int ProductId { get; set; }
    public int Qty { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Amount { get; set; }

    public byte[] Version { get; set; }
           
   [ForeignKey("ProductId")]
   public virtual Product ProductX { get; set; }
}


That's how we will also tackle NHibernate model-binding-mismatch. But with a twist, we will do it in reverse instead, we will introduce primitive types, so we can directly bind them to UI widgets. An example implementation:

public class SalesDetail 
{
    public virtual SalesHeader SalesHeader { get; set; }

    public virtual int SalesDetailId { get; set; }
    public virtual Product ProductX { get; set; }
    public virtual int Qty { get; set; }
    public virtual decimal UnitPrice { get; set; }
    public virtual decimal Amount { get; set; }

    public virtual byte[] Version { get; set; }            

    [Lookup("ProductId")]
    public virtual int? lookup_ProductX { get; set; }
}



That's very clean compared to EF's approach, NH's approach directly mimics the problem domain as we still use the ProductX as a pointer to Product entity; UI-concerns-wise, we just need to introduce a new property on our model whenever we need something for the widgets to bind upon.


However, there's a problem with NHibernate populating those properties at the same time. It cannot bind a field to two properties. So for that, we have to do this:


After you get the records from NHibernate:
foreach (SalesDetail d in sh.Sales)
{ 
 d.lookup_ProductX.Value = d.ProductX.ProductId;
}

Before you save the records via NHibernate:
foreach (SalesDetail d in sh.Sales)
{ 
 d.ProductX = s.Load<Product>(d.lookup_ProductX.Value);
}



But that easily gets old, we need to make a helper to automatically re-hydrate those properties.


This is our NHibernate assigner helper, it's very short and concise:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate;
using System.Reflection;
using System.Collections;

namespace NhLookupAssigner
{
    public static class Helper
    {
        public static void SetupModel(this ISession s, object model)
        {
            Type modelType = model.GetType();

            foreach (PropertyInfo p in modelType.GetProperties())
            {
                LookupAttribute a = p.GetCustomAttributes(false).OfType<LookupAttribute>().SingleOrDefault();

                if (a != null)
                {
                    string lookupName = p.Name.Substring(p.Name.IndexOf('_') + 1);

                    PropertyInfo targetProperty = modelType.GetProperty(lookupName);
                    Type targetPropertyType = targetProperty.PropertyType;

                    // get property's(public virtual Product Product { get; set; }) object
                    object nhValue = modelType.InvokeMember(lookupName, BindingFlags.GetProperty, null, model, new object[] { });
                    // get property's object's primary key
                    object pkValue = nhValue.GetType().InvokeMember(a.PrimaryKey, BindingFlags.GetProperty, null, nhValue, new object[] { });


                    // set property(e.g. public virtual int? lookup_Product { get; set; }) value
                    modelType.InvokeMember(p.Name, BindingFlags.SetProperty, null, model, new object[] { pkValue });

                    
                }
                else
                {
                    if (p.PropertyType.IsGenericType && typeof(IEnumerable).IsAssignableFrom(p.PropertyType))
                    {
                        object list = modelType.InvokeMember(p.Name, BindingFlags.GetProperty, null, model, new object[] { });
                        foreach (var x in (IEnumerable)list)
                        {
                            s.SetupModel(x);
                        }
                    }//if
                }
            }//foreach
            
        }//void SetupModel

        

        public static void SetupNh(this ISession s, object model)
        {            
            Type modelType = model.GetType();

            foreach (PropertyInfo p in modelType.GetProperties())
            {
                LookupAttribute a = p.GetCustomAttributes(false).OfType<LookupAttribute>().SingleOrDefault();

                if (a != null)
                {
                    string lookupName = p.Name.Substring(p.Name.IndexOf('_') + 1);

                    PropertyInfo targetProperty = modelType.GetProperty(lookupName);
                    Type targetPropertyType = targetProperty.PropertyType;
                    object inputValue = modelType.InvokeMember(p.Name, BindingFlags.GetProperty, null, model, new object[] { });

                    object nhValue = s.Load(targetPropertyType, inputValue);
                    modelType.InvokeMember(lookupName, BindingFlags.SetProperty, null, model, new object[] { nhValue });
                }
                else
                {                    
                    if (p.PropertyType.IsGenericType && typeof(IEnumerable).IsAssignableFrom(p.PropertyType))
                    {                                                
                        object list = modelType.InvokeMember(p.Name, BindingFlags.GetProperty, null, model, new object[] { });
                        foreach (var x in (IEnumerable)list)
                        {
                            s.SetupNh(x);
                        }                       
                    }//if
                }
            }//foreach

        }//void SetupNh

    }//class Helper

}//namespace NhLookupAssigner



And this is the Lookup attribute:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NhLookupAssigner
{
    [AttributeUsage(AttributeTargets.Property)]
    public class LookupAttribute : Attribute
    {
        public string PrimaryKey { get; set; }

        public LookupAttribute(string primaryKey)
        {
            this.PrimaryKey = primaryKey;
        }
    }
}

To use on opening:
void DoOpen()
{
    using (var s = Mapper.GetSessionFactory().OpenSession())
    {
        var z = s.Query<SalesHeader>().Where(x => x.SalesHeaderId == int.Parse(uxRecordIdentifier.Text)).Single()

        s.SetupModel(z);

        bdsHeader.DataSource = z;
    }
}

To use on saving:
void DoSave()
{
    using (var s = Mapper.GetSessionFactory().OpenSession())
    using (var tx = s.BeginTransaction())
    {
        var sh = (SalesHeader)bdsHeader.Current;

        s.SetupNh(sh);

        s.SaveOrUpdate(sh);

        tx.Commit();
    }
}

No comments:

Post a Comment