Tuesday 6 April 2010

Implementation of custom types within the mapping of E4 POCO DAL

I have a data access layer that I am replacing with an E4 POCO DAL. For historical reasons the database has some custom data types such as a Boolean that has been implemented in the database as a string that contains ‘True’ or ‘False’. We want the business entities to have strongly typed properties, in other words a boolean.

We opened up our EDM and wanted to change the data type of the mapped field sso that on the database side it is a string and on the business logic side it is a Boolean. The editor of VS2010 RC2 even allows you to do this but when you generate the POCOs you will end up with a compile time error. Mike Taulty experienced the same problem in his blog: http://mtaulty.com/CommunityServer/blogs/mike_taultys_blog/archive/2008/02/14/10182.aspx

NHybernate has an extensible model where you can implement IUserType to do this kind of custom types. Since E4 is not quite as extensible we decided to modify the T4 template to generate an additional property who’s setter sets the value of the string value. This is quite fiddly since the template is like a script with a lot of if statements, actually a long way from an OO style of programming. We replaced lines 159-160 with

<#
        // NOTE: added for simple test to map string column to bool type
        if (code.FieldName(edmProperty).EndsWith("StringFlag"))
        {
#>
    }
    <#=PropertyVirtualModifier(Accessibility.ForProperty(edmProperty))#> bool <#=code.Escape(edmProperty).Replace("StringFlag", "")#>
    {
        <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get
        {
            bool result = false;
            bool.TryParse(<#=code.Escape(edmProperty)#>, out result);
            return result;
        }

        <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set
        {
            <#=code.Escape(edmProperty)#> = value.ToString();
        }
<#
        }
    }
#>

What this does is that if we rename a property of a POCO database field to have the ending “StringFlag” it adds another property as shown below:

    public virtual string IsFloridaOnlyStringFlag
    {
        get;
        set;
    }
    public virtual bool IsFloridaOnly
    {
        get
        {
            bool result = false;
            bool.TryParse(IsFloridaOnlyStringFlag, out result);
            return result;
        }

        set
        {
            IsFloridaOnlyStringFlag = value.ToString();
        }
    }

 

In order to get the layout of the generated code to be nice the T4 template looks a little unreadable! We did not set the modifier of the StringFlag to protected because E4 needs to reflect and find this field. So this means unfortunately our business entities will have the field that we are trying to replace. In our case this is ok because we will systematically refactor the database model as we would do any other code.

The other interesting problem we had came when we wanted to rename the business entity corresponding to the table name. For Example MOP_MODEL_RUN should become ModelRun. In this way we can satisfy the FxCop code analysis rules. The problem came in the following code:

public IList<TEntity> GetAll<TEntity>()
{

    IList<TEntity> list = this.context
        .CreateQuery<TEntity>(
        "[" + typeof(TEntity).Name + "]")
        .ToList();
    return list;
}

This failed because typeof(TEntity).Name returned the business entity name of the table and not the database table name. Unfortunately the mapping information between the database world and the business entity world is quite hard to get, it is surprising that there almost nobody out there complaining about this. To fix this we changed the above code to:

public IList<TEntity> GetAll<TEntity>()
{
    string entityType = GetEntitySetName(typeof(TEntity));

    IList<TEntity> list = this.context
        .CreateQuery<TEntity>(
        /* "[" + */ entityType /* + "]" */)
        .ToList();
    return list;
}

We cache this mapping in a dictionary so we added:

private IDictionary<Type, string> objectSetNameMap = new Dictionary<Type, string>();

and

private string GetEntitySetName(Type entityType)
{
    if (!this.objectSetNameMap.ContainsKey(entityType))
    {
        this.objectSetNameMap[entityType] = EntityHelper.GetEntitySetName(entityType, this.context);
    }
    return this.objectSetNameMap[entityType];
}

And here is the helper class that figures out the mapping between the business entities and the database entities:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Metadata.Edm;
using System.Reflection;
using System.Data.Objects;
using System.ComponentModel;

namespace Ccp.DataAccessLayer
{
    public static class EntityHelper
    {
        private static void LoadAssemblyIntoWorkspace(MetadataWorkspace workspace, Assembly assembly)
        {
            workspace.LoadFromAssembly(assembly);
        }

        #region GetEntitySetName

        public static string GetEntitySetName(Type entityType, ObjectContext context)
        {
            EntityType edmEntityType = GetEntityType(context, entityType);
            EntityContainer container = context.MetadataWorkspace.GetItems<EntityContainer>(DataSpace.CSpace).Single<EntityContainer>();
            EntitySet set = (EntitySet)container.BaseEntitySets.Single<EntitySetBase>(delegate(EntitySetBase p)
            {
                return (p.ElementType == edmEntityType);
            });
            return (container.Name + "." + set.Name);
        }

        #endregion

        #region GetEntityType
        public static EntityType GetEntityType(ObjectContext context, Type clrType)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (clrType == null)
            {
                throw new ArgumentNullException("clrType");
            }
            EdmType type = null;
            try
            {
                type = context.MetadataWorkspace.GetType(clrType.Name, clrType.Namespace, DataSpace.OSpace);
            }
            catch (ArgumentException)
            {
                LoadAssemblyIntoWorkspace(context.MetadataWorkspace, clrType.Assembly);
                type = context.MetadataWorkspace.GetType(clrType.Name, clrType.Namespace, DataSpace.OSpace);
            }
            return (EntityType)context.MetadataWorkspace.GetEdmSpaceType((StructuralType)type);
        }

        public static bool TryGetEntityType(ObjectContext context, Type clrType, out EntityType entityType)
        {
            entityType = null;
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }
            if (clrType == null)
            {
                throw new ArgumentNullException("clrType");
            }
            EdmType type = null;
            bool flag = context.MetadataWorkspace.TryGetType(clrType.Name, clrType.Namespace, DataSpace.OSpace, out type);
            if (!flag)
            {
                LoadAssemblyIntoWorkspace(context.MetadataWorkspace, clrType.Assembly);
                flag = context.MetadataWorkspace.TryGetType(clrType.Name, clrType.Namespace, DataSpace.OSpace, out type);
            }
            if (flag)
            {
                entityType = (EntityType)context.MetadataWorkspace.GetEdmSpaceType((StructuralType)type);
                return true;
            }
            return false;
        }
        #endregion

        #region GetReferenceProperty

        public static PropertyDescriptor GetReferenceProperty(PropertyDescriptor pd)
        {
            return GetReferenceProperty(pd, TypeDescriptor.GetProperties(pd.ComponentType).Cast<PropertyDescriptor>());
        }

        public static PropertyDescriptor GetReferenceProperty(PropertyDescriptor pd, IEnumerable<PropertyDescriptor> properties)
        {
            string refPropertyName = pd.Name + "Reference";
            return properties.SingleOrDefault<PropertyDescriptor>(delegate(PropertyDescriptor p)
            {
                return (p.Name == refPropertyName);
            });
        }
        #endregion

    }}