Tuesday, 20 April 2010

Today’s journey in the world of EF DALs

Here is a small story of debugging and developing a POCO EF4 DAL. It starts of with this piece of code

          CED_COUNTRY country =  this.databaseContext
                .Query<CED_COUNTRY>()
                .Where(c => c.REGION_ID.Equals(region.REGION_ID))
                .First();

This produced the error:
“Unable to create a constant value of type 'System.Object'. Only primitive types ('such as Int32, String, and Guid') are supported in this context.”

The mistake was that REGION_ID is nullable and could be fixed by

           CED_COUNTRY country =  this.databaseContext
                .Query<CED_COUNTRY>()
                .Where(c => c.REGION_ID.Value.Equals(region.REGION_ID))
                .First();

Next I was interested in being able to debug the Linq expression. By using F11 and step into we can step into Query extension methods such as Query but we are not able to step into the Func expression in the where clause. After some experimentation we found that it is not possible to step into a Func but it is possible to step into an action. We decided to make some Query extension methods for pure debugging purposes. The idea is that the Linq expression can be modified with these additional debugging extension methods in a similar way to the T in a piped unix command.
To debug the above example you can edit the linq expression to look like

            CED_COUNTRY country =  this.databaseContext
                .Query<CED_COUNTRY>()
                .Debug()
                .Where(e)
                .Debug()
                .First();

The corresponding extension methods look like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CCP.DataAccessLayerTest
{
    public static class EnumerableExtensions
    {
        public static IQueryable<T> Debug<T>(this IQueryable<T> instance)
        {
            // do stuff to debug here, like
            Assert.IsNotNull(instance);
            Console.WriteLine(instance.Count());
            return instance;
        }
    }
}


Next we had a problem when updating a string that is defined as a varchar(100) in the database. As we persist a field with more than 100 characters into the database we got a truncation error.
We first looked at the EDM to determine if there was some truncation or validation options. Unfortunately the EDM did not have these options. Next we considered modify the setter in the a attribute driven way as shown below.

http://msdn.microsoft.com/en-us/library/cc309320%28v=MSDN.10%29.aspx

using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;
public class Customer
{
    [StringLengthValidator(0, 20)]
    public string CustomerName;
    public Customer(string customerName)
    {
        this.CustomerName = customerName;
    }
}
public class MyExample
{
    public static void Main()
    {
        Customer myCustomer = new Customer("A name that is too long");
        ValidationResults r = Validation.Validate<Customer>(myCustomer);
        if (!r.IsValid)
        {
            throw new InvalidOperationException("Validation error found.");
        }
    }
}
In our case this would look like

namespace Ccp.Entities
{
    public class Validation
    {
        public static IEnumerable<ValidationResult> Validate(object component)
        {
            return from descriptor in
TypeDescriptor.GetProperties(component).Cast<PropertyDescriptor>()
                    from validation in descriptor.Attributes.OfType<ValidationAttribute>()
                    where !validation.IsValid(descriptor.GetValue(component))
                    select new ValidationResult(
                        validation.ErrorMessage ?? string.Format(CultureInfo.CurrentUICulture, "{0} validation failed.", validation.GetType().Name),
                        new[] { descriptor.Name });
        }
    }
}

With our T4 template modified to add a compiler attribute looking like:

     [StringLength(100)]
        public virtual string SUBMISSION_NAME
        {
            get;
            set;
        }

But this would mean adding validation everywhere which will have a performance hit. Also this would mean that we need to make another alteration to the T4 template.
Here is how we solved this problem.:

1.    Create an interface for the validator and IEntityRectifier

namespace Ccp.Entities
{
    public interface IEntityValidator
    {
        IEnumerable<ValidationResult> Validate();
    }
}
namespace Ccp.Entities
{
    public interface IEntityRectifier
    {
        void Rectify();
    }
}

2.    Create a partial class that extends the POCO that you want to validate and implement the interfaces as appropriate

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
namespace Ccp.Entities
{
    public partial class SUB_SUBMISSION : IEntityValidator, IEntityRectifier
    {
        #region IEntityValidator Members
        public IEnumerable<ValidationResult> Validate()
        {
            if (IsSubscriptionNameTooLong())
            {
                yield return new ValidationResult("SUBMISSION_NAME should not be longer than 100");
            }
            if (this.CEDANT_ID < 0)
            {
                yield return new ValidationResult("CEDANT_ID should not be less than 0");
            }
        }
        private bool IsSubscriptionNameTooLong()
        {
            return this.SUBMISSION_NAME.Length > 100;
        }
       #endregion
        #region IEntityRectifier Members
        public void Rectify()
        {
            if (IsSubscriptionNameTooLong())
            {
                this.SUBMISSION_NAME = this.SUBMISSION_NAME.Substring(0, 100);
            }
        }
        #endregion
    }
}

3. In the generic DataBaseContext add the following procedures to validate and rectify properties. Note that if the interface has not been implemented the validation or rectify will not happen.

       private static void ValidateEntity(object entity)
       {
            IEntityValidator validator = entity as IEntityValidator;
            if (validator != null)
            {
                foreach (ValidationResult result in validator.Validate())
                {
                    Console.WriteLine(result);
                }
            }
        }

        private static void RectifyEntity(object entity)
        {
            IEntityRectifier rectifier = entity as IEntityRectifier;
            if (rectifier != null)
            {
                rectifier.Rectify();
            }
        }

4. Invoke Validate and Rectify from the Add and save methods
        public void Add(params object[] entities)
        {
            foreach (object entity in entities)
            {
                ValidateEntity(entity);
                RectifyEntity(entity);
                string entityType = GetEntitySetName(entity.GetType());
                context.AddObject(entityType, entity);
            }
        } 
      public void Save()
        {
            foreach (ObjectStateEntry objectStateEntry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified|EntityState.Added|EntityState.Unchanged|EntityState.Deleted))
            {
                ValidateEntity(objectStateEntry.Entity);
                RectifyEntity(objectStateEntry.Entity);
            }
            int result = context.SaveChanges();
        }

Unfortunately the ObjectStateManager has some problems in getting only Modified entries. This is probably because the POCO is ignorant of the database and therefore the ObjectStateManager cannot determine if the object has been modified. I we used the EDM database context without POCO this would probably work without problems

Advantages of this method
- Optional interface: You only need to implement it where you need it
- It's still POCO and database ignorant
- We can combine several properties to generates more advanced validation
- Better performing than using a declarative approach and reflection

Disadvantages
The Object state manager does not seem to work as expected.
When we change the ORM table naming and regenerate the POCO the partial class will not generate a compiler error. We could introduce a property that is addressed by the partial class, in this way we would get a desirable compiler error. But we decided this is an unclean approach and we will either live with it or find an alternative approach.