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.
Tags: