Monday 9 December 2013

Shortcomings of Entity Framework

The first time I used Entity Framework, I was blown away by how simple it was to use, and how quickly I could get up and running. Until recently, most of my work has revolved around NHibernate, which in comparison is far more complex. For some reason, it did not occur to me that this simplicity would bring with it some inflexibility.

One issue that has bitten me recently is the lack of an equivalent to IUserType. NHibernate understands how to map a simple type from a database field to a property in an entity, but what if it is a more complex type stored in an XML column, or what if the data is coming from a web service? The way to achieve this in NHibernate is explained very well in this post.

This is a useful feature (that should be used sparingly) that I just assumed would be available in Entity Framework. Strictly speaking it isn't, although there is a work around.

Take the following objects:
public class Parent
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    public virtual Child Child {get;set;}
}

public class Child
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    // And other stuff...
}
I want to persist the Parent in a row in a SQL Server table, and persist the Child in a an XML column in that row:
CREATE TABLE [Parent](
[Id] [uniqueidentifier] NOT NULL PRIMARY_KEY,
[Description] [nvarchar](50) NULL,
[Child] [xml] NULL
This would have been possible in NHibernate with IUserType, but with Entity Framework we have to do things differently.

Firstly, unfortunately, this impacts on our domain model - never a good thing for an O/RM to inflict. It will have to look like this:
public class Parent
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    public virtual string ChildXml {get;set;}
    public virtual Child Child {get;set;}
}

public class Child
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    // And other stuff...
}
And I have renamed the column in the database:
CREATE TABLE [Parent](
[Id] [uniqueidentifier] NOT NULL PRIMARY_KEY,
[Description] [nvarchar](50) NULL,
[ChildXml] [xml] NULL
)
Now, in out class that overrides DbContext, we need to intercept the creation and saving of this entity. Intercepting the creation is done by handling the ObjectMaterialized event of the ObjectContext, and in here we construct our child entity from the xml:
public class Context : DbContext
{
    public Context()
    {
         //...
         ObjectContext.ObjectMaterialized += new ObjectMaterializedEventhandler(ObjectContext_ObjectMaterialized);
    }

    //...

    public ObjectContext ObjectContext
    {
        get { return ((IObjectContextAdapter)this).ObjectContext; }
    }

    public void ObjectContext_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
    {
        var parent = e.Entity as Parent;

        if (parent != null)
            parent.Child = XmlObjectSerializer.Deserialize(applicationForm.XmlData);
    }

    //...
}
And for saving, we need to override the SaveChanges() method of DbContext, as descibed by Chris McKenzie in this post.
public class Context : DbContext
{
    private void InterceptBefore(ObjectStateEntry item)
    {
        var parent = item.Entity as Parent;

        if (parent!= null)
            parent.XmlData = XmlObjectSerializer.Serialize(applicationForm.Child);
    }

    public override int SaveChanges()
    {
        const EntityState entitiesToTrack = EntityState.Added | EntityState.Modified | EntityState.Deleted;

        var elementsToSave =
            this.ObjectContext
                .ObjectStateManager
                .GetObjectStateEntries(entitiesToTrack)
                .ToList();

        elementsToSave.ForEach(InterceptBefore);
        var result = base.SaveChanges();
        return result;
    }
}
Now if I want to display a list of Parent entities with just their description, this could all become very inefficient. What is needed is some way of lazy loading the child. This could mean the child is a separate entity mapped to a table with its own XML field, but what about in this scenario?
public class Parent
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    public virtual string ChildrenXml {get;set;}
    public virtual ICollection Children {get;set;}
}

public class Child
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    // And other stuff...
}
And I have renamed the column in the database:
CREATE TABLE [Parent](
[Id] [uniqueidentifier] NOT NULL PRIMARY_KEY,
[Description] [nvarchar](50) NULL,
[ChildrenXml] [xml] NULL
)
This rules out the previous option. So surely it's just a simple case of setting the ChildrenXml property to be lazy loaded? Again (and I think more justifiably) I just assumed this would be possible in Entity Framework. I was somewhat surprised to learn that Entity Framework doesn't support lazy loading of properties.

again, the solution is to change our Domain to handle this:
public class Parent
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    public virtual Guid DetailsId {get;set;}
    public virtual ParentDetails Details {get;set;}
}

public class ParentDetails
{
    public virtual Guid Id {get;set;}
    public virtual string ChildrenXml {get;set;}
    public virtual ICollection Children {get;set;}
}

public class Child
{
    public virtual Guid Id {get;set;}
    public virtual string Description {get;set;}
    // And other stuff...
}
And I have renamed the column in the database:
CREATE TABLE [Parent](
[Id] [uniqueidentifier] NOT NULL PRIMARY_KEY,
[Description] [nvarchar](50) NULL,
[DetailsId] [uniqueidentifier] NOT NULL
)

CREATE TABLE [Parent](
[Id] [uniqueidentifier] NOT NULL PRIMARY_KEY,
[ChildrenXml] [xml] NULL
)
This seems to be the officially sanctioned way of doing things. If you have a large field - a BLOB, a VARBINARY, a VARCHAR(MAX), it has to go in a 'Details' table. Surely an O/RM on version 6 should have this functionality?