Oct 30 2011

Asp.net MVC3: entity framework 4.1 UniqueValue attribute validator

Category: ASP.NET | C# | Linq | MVC3 — Duke @ 15:28

I’m playing with MVC3 and EF4.1 (magic unicorn version) for a nice project I’m working on, and I’ve found myself in the situation to validate some fields against value duplication

(think about username, or other things that must be unique).

Unfortunately we don't have [Unique] constraint attribute in EF4.1 yet, so I’ve thought to leverage on the validation infrastructure of MVC3 to ensure uniqueness of the values.

I like the simplicity and expressiveness of the attributes then I’ve implemented my UniqueValueAttribute class.

this is remarkable because now I have a simple way to decorate every model attribute I want to be unique

(lot of fun code follows)

namespace Domain.Validators
{
    using System;
    using System.Linq;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity;
    using System.Linq.Expressions;
    using System.Collections.Generic;
    using System.Reflection;

    [AttributeUsage(AttributeTargets.Property)]
    public class UniqueValueAttribute : ValidationAttribute
    {

        private const string Where = "System.Linq.IQueryable`1[TSource] Where[TSource](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Boolean]])";
        
        private const string Count = "Int32 Count[TSource](System.Linq.IQueryable`1[TSource])";            

        private int total = 0;

        private Type dbContextType;

        private IEnumerable<string> keys;

        public UniqueValueAttribute(Type dbContextType, params string[] keys)
        {
            if (!typeof(DbContext).IsAssignableFrom(dbContextType)) throw new ArgumentException("Only dbCOntext derived type could be used as parameter");
            this.dbContextType = dbContextType;
            this.keys = new List<string>(keys);
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {            
            // creates an instance of the database context
            var dbContext = (DbContext) Activator.CreateInstance(dbContextType);

            // retrieves the dbset related values

            //Type dbSet = typeof(DbSet<>).MakeGenericType(validationContext.ObjectType);    

            var property = 
                dbContextType
                .GetProperties()
                .Where(
                    p => 
                        (p.PropertyType.IsGenericType) && 
                        (p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) && 
                        (p.PropertyType.GetGenericArguments()[0].IsAssignableFrom(validationContext.ObjectType))
                ).Single();

            IQueryable repository = (IQueryable)property.GetValue(dbContext, null);

            var propertyType = property.PropertyType.GetGenericArguments()[0];

            // prepares Linq extension methods calls for Where and Count to use in the query expression
            var queryableWhere = GetQueryableMethod(propertyType, Where);

            var queryableCount = GetQueryableMethod(propertyType, Count);

            // prepares member access info and parameters for the lambda
            var uniqueMemberInfo = GetMember(propertyType, validationContext.DisplayName);

            var paramX = Expression.Parameter(propertyType, "x");

            // set up the core of the lambda expression (invariant for the key part)
            Expression lambda =                 
                    Expression.Equal(
                        Expression.MakeMemberAccess(
                            paramX,
                            uniqueMemberInfo),
                        Expression.Constant(value));


            // chain up "AND !=" parts for each key
            foreach (var keyPart in keys)
            {
                var keyPartMemberInfo = GetMember(propertyType, keyPart);

                object keyPartValue = propertyType.GetProperty(keyPart).GetValue(validationContext.ObjectInstance, null);

                lambda = 
                    Expression.AndAlso(
                        lambda, 
                        Expression.NotEqual(
                            Expression.MakeMemberAccess(
                                paramX,
                                keyPartMemberInfo),
                            Expression.Constant(keyPartValue)));

            }

            // set up the lambda and the parameter
            lambda = Expression.Lambda(lambda, paramX);

            // set up the count query
            var countQuery =
                Expression.Call(
                    queryableCount,
                    Expression.Call(
                        queryableWhere,
                        repository.Expression,
                        lambda));

            // executes the count query
            total = (int)repository.Provider.Execute<int>(countQuery);
           
            // validate the query result
            if (total == 0) return null;
            
            ErrorMessage = String.Format("The value {0} already exists", value.ToString());

            return new ValidationResult(ErrorMessage, new[] { validationContext.DisplayName });                                  
        }      

        protected static MethodInfo GetQueryableMethod(Type type, string signature)
        {
            return 
                typeof(System.Linq.Queryable)
                .GetMethods()
                .Where(x => x.ToString() == signature)
                .Single()
                .MakeGenericMethod(type);
        }

        protected static MemberInfo GetMember(Type type, string memberName)
        {
            return
               type
               .GetMembers()
               .Where(x => x.Name == memberName)
               .Single();
        }
    }
}

then to use it just apply the decorator on the field you need to validate against uniqueness:

public class User    
{    
    public virtual Guid Id { get; set; }    
    [UniqueValue(typeof(ApplicationDb), "Id")]    
    public virtual string Username { get; set; }    
}
 
 

note: the attribute constructor takes the type of the DbContext class that implements the datacontext, it also allows you to specify which the keys of your entity are, so to support the Update scenario where it is correct to validate another entity match with the same value if the matching entity is the entity itself.

in this verion the connection string need to have a name exactly equal to the dbcontext class and there is no way to specify another, even if it is very easy to modify the attribute class to consume different connection string.

Tag di Technorati: ,,,,

Tags:

Comments (1) -

1.
Matteo Migliore Matteo Migliore Italy says:

Hi Doc!

With this solution the domain can't stay in an assembly separated from the DAL.

Also, I think that it don't work also for multiple unique columns and multiple columns unique (e.g. CF is unique and Name+Lastname is unique).

Matteo.

2.
Duke Duke Italy says:

observation 1) partially right, partially wrong.
depends on the size of the project you are working on. in small sized project (from which this solution cames from) there is no gain in having separate assemblies for DAL and domanin. btut i agree with you that conceptually itis mor beauty to have it in separate assemblies.

2) right, the purpose of this attribute is to make unique only one column in regards of the valueof another column (but in this case they could be multiple)

abot the point 1) you could make it independant by passing the fully qualified name of the data context object as string and then activate it, but i don't like type code that can't be easilly refactored.

unfortunately i don't have any other idea on how to make the context independent fromthe dal given that the check must be made against the dal (it is the purpose of this attribute) so in some way there must be a link between the two

maybe a database context could be registered globally somewhere and then used by this attribute?
it could work but then you have to remember to register it, i was searching for a solution that allows me to be very specific in a single point incode without any configuration.

Any idea?

3.
eurostorehelp.com eurostorehelp.com says:

Great post. I was checking continuously this blog and I am impressed! Very helpful info particularly the last part Smile I care for such info much. I was looking for this particular info for a long time. Thank you and best of luck.

Look at my homepage :: mountain dew und erektile Dysfunktion ( eurostorehelp.com - http://eurostorehelp.com/kaufencialisonlinede1/ )

4.
Vad som orsakar bakterieinfektioner Vad som orsakar bakterieinfektioner says:

What's up, I want to subscribe for this web site to obtain hottest updates, so where can i do it please help.

Here is my web page ::  Vad som orsakar bakterieinfektioner - http://lamedecinefrancaise.com/kop-cipro-se-1/

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading