UniqueAttribute validation modifications based on this article: http://blogs.microsoft.co.il/blogs/shimmy/archive/2012/01/23/validationattribute-that-validates-a-unique-field-against-its-fellow-rows-in-the-database.aspx
The above code is based on the following resources:
This article explains how to create a new DataAttribute [Unique] whe can be used on properties of Entity Framework models. Upon Database initilization the models [Unique] properties are turned into unique indexes on the database. http://code.msdn.microsoft.com/windowsdesktop/CSASPNETUniqueConstraintInE-d357224a
This article (with modified code attached) creates validators in the business logic based on the models [Unique] attribute which then provide a nice wrror message to the user. http://blogs.microsoft.co.il/blogs/shimmy/archive/2012/01/23/validationattribute-that-validates-a-unique-field-against-its-fellow-rows-in-the-database.aspx
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Reflection;
using System.Data.Entity.ModelConfiguration.Conventions;
using PcnWeb.Helpers;
using System.Data.Objects;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
///
/// from: http://code.msdn.microsoft.com/windowsdesktop/CSASPNETUniqueConstraintInE-d357224a
///
namespace System.ComponentModel.DataAnnotations
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class UniqueAttribute : ValidationAttribute
{
private readonly Type _ContextType;
public Type ContextType
{
get
{
return _ContextType;
}
}
//TODO: If placed in your domain, uncomment and replace MyDbContext with your domain's DbContext/ObjectContext class.
public UniqueAttribute() : this(typeof(PcnWeb.Models.PCNWebContext)) { }
/// <summary>
/// Initializes a new instance of <see cref="UniqueAttribute"/>.
/// </summary>
/// <param name="contextType">The type of <see cref="DbContext"/> or <see cref="ObjectContext"/> subclass that will be used to search for duplicates.</param>
public UniqueAttribute(Type contextType)
{
if (contextType == null)
throw new ArgumentNullException("contextType");
if (!contextType.IsSubclassOf(typeof(DbContext)) && !contextType.IsSubclassOf(typeof(ObjectContext)))
throw new ArgumentException("The contextType Type must be a subclass of DbContext or ObjectContext.", "contextType");
if (contextType.GetConstructor(Type.EmptyTypes) == null)
throw new ArgumentException("The contextType type must declare a public parameterless consructor.");
_ContextType = contextType;
}
/// <summary>
/// Validates the value against the matching columns in the other rows of this table.
/// Note that this method does not validate null or empty strings.
/// </summary>
/// <param name="value">The value to validate</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>An instance of the <see cref="ValidationResult"/> class.</returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null || (value is string && string.IsNullOrWhiteSpace((string)value))) return ValidationResult.Success;
var type = validationContext.ObjectType;
var property = type.GetProperty(validationContext.DisplayName);//MemberName
type = property.DeclaringType;
using (var dbcontext = (IDisposable)Activator.CreateInstance(_ContextType))
{
var context = dbcontext is DbContext ? ((IObjectContextAdapter)dbcontext).ObjectContext : (ObjectContext)dbcontext;
var md = context.MetadataWorkspace;
var entityType = md.GetItems<EntityType>(DataSpace.CSpace).SingleOrDefault(et => et.Name == type.Name);
while (entityType.BaseType != null)
entityType = (EntityType)entityType.BaseType;
var objectType = typeof(object);
var isInherited = false;
var baseType = type;
while (baseType.Name != entityType.Name && baseType.BaseType != objectType)
{
baseType = baseType.BaseType;
isInherited = true;
}
var methodCreateObjectSet = typeof(ObjectContext).GetMethod("CreateObjectSet", Type.EmptyTypes).MakeGenericMethod(baseType);
var baseObjectSet = (ObjectQuery)methodCreateObjectSet.Invoke(context, new object[] { });
var objectSet = baseObjectSet;
var setType = baseObjectSet.GetType();
if (isInherited)
{
var ofType = setType.GetMethod("OfType");
ofType = ofType.MakeGenericMethod(type);
objectSet = (ObjectQuery)ofType.Invoke(baseObjectSet, null);
setType = objectSet.GetType();
}
var methodWhere = setType.GetMethod("Where");
var eSql = string.Format("it.{0} = @{0}", validationContext.DisplayName);
var query = (ObjectQuery)methodWhere.Invoke(objectSet,
new object[] { eSql, new[] { new ObjectParameter(validationContext.DisplayName, value) } });
var result = query.Execute(MergeOption.NoTracking).Cast<object>();
bool isValid = true;
using (var enumerator = result.GetEnumerator())
{
if (enumerator.MoveNext())
{
var nameProperty = typeof(ObjectSet<>).MakeGenericType(baseType).GetProperty("EntitySet");
var entitySet = (EntitySet)nameProperty.GetValue(baseObjectSet, null);
var entitySetName = entitySet.Name;
do
{
var current = enumerator.Current;
var curKey = context.CreateEntityKey(entitySetName, current);
var validatingKey = context.CreateEntityKey(entitySetName, validationContext.ObjectInstance);
if (curKey != validatingKey)
{
isValid = false;
break;
}
} while (enumerator.MoveNext());
}
}
return isValid ?
ValidationResult.Success :
new ValidationResult(
string.Format("There is already a '{0}' record that has its '{1}' field set to '{2}'.",
validationContext.ObjectType.Name,
validationContext.DisplayName,
value),
new[] { validationContext.DisplayName });
}
}
}
}
namespace PcnWeb.Helpers
{
/// <summary>
/// Try to analyze the ModelEntity by reflecting them one by one and then
/// fetch the properties with "unique" attribute and then do calling Sql statement to create database.
/// Based on :http://stackoverflow.com/a/10566485/429544
/// </summary>
/// <typeparam name="T"></typeparam>
public class MyDbGenerator<T> : IDatabaseInitializer<T> where T : DbContext
{
public void InitializeDatabase(T context)
{
context.Database.Delete();
context.Database.Create();
//Fetch all the father class's public properties
var fatherPropertyNames = typeof(DbContext).GetProperties().Select(pi => pi.Name).ToList();
//Loop each dbset's T
foreach (PropertyInfo item in typeof(T).GetProperties().Where(p => fatherPropertyNames.IndexOf(p.Name) < 0).Select(p => p))
{
//fetch the type of "T"
Type entityModelType = item.PropertyType.GetGenericArguments()[0];
var allfieldNames = from prop in entityModelType.GetProperties()
where prop.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.UniqueAttribute), true).Count() > 0
select prop.Name;
foreach (string s in allfieldNames)
{
context.Database.ExecuteSqlCommand("alter table " + entityModelType.Name.pluralize() + " add unique(" + s + ")");
}
}
}
}
}