DCSIMG
ValidationAttribute that validates a unique field against its fellow rows in the database - Shimmy on .NET

ValidationAttribute that validates a unique field against its fellow rows in the database

UPDATE: Jan 30 2012: Added check that entity is not itself (i.e. when user attempts to save the same entity).
UPDATE: Feb 26 2012: Added support for inherited entities, where the validated property is on the sub-class of the inheritance hierarchy.

Here is a ValidationAttribute subclass that will allow you to validate that a column doesn't have duplicates.

It's intended to be used with EF 4.1 DbContext, or with the EF4's ObjectContext.

namespace System.ComponentModel.DataAnnotations
{
#if !SILVERLIGHT
  using System;
  using System.Data.Entity;
  using System.Data.Entity.Infrastructure;
  using System.Data.Metadata.Edm;
  using System.Data.Objects;
  using System.Linq;
#endif
  /// <summary>   /// Validates whether a value is set in another row at the same column.   /// Does not validate null empty or whitespace strings.   /// </summary>   [AttributeUsage(AttributeTargets.Property)]   public class UniqueAttribute : ValidationAttribute   { #if SILVERLIGHT        /// <summary>     /// Just a body method, the ValidationAttribute requires the IsValid method to be overridden.     /// </summary>     protected override ValidationResult IsValid(object value, ValidationContext validationContext)     {       return ValidationResult.Success;     } #else     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(MyDbContext)) { }
    /// <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 valueValidationContext validationContext)
    {
      if (value == null || (value is string && string.IsNullOrWhiteSpace((string)value))) return ValidationResult.Success;
 
      var type = validationContext.ObjectType;
      var property = type.GetProperty(validationContext.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(contextnew object[] { });
        var objectSet = baseObjectSet;
        var setType = baseObjectSet.GetType();
 
        if (isInherited)
        {
          var ofType = setType.GetMethod("OfType");
          ofType = ofType.MakeGenericMethod(type);
          objectSet = (ObjectQuery)ofType.Invoke(baseObjectSetnull);
          setType = objectSet.GetType();
        }
 
        var methodWhere = setType.GetMethod("Where");
 
        var eSql = string.Format("it.{0} = @{0}"validationContext.MemberName);
 
        var query = (ObjectQuery)methodWhere.Invoke(objectSet,
          new object[] { eSqlnew[] { new ObjectParameter(validationContext.MemberNamevalue) } });
 
        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(baseObjectSetnull);
            var entitySetName = entitySet.Name;
 
            do
            {
              var current = enumerator.Current;
              var curKey = context.CreateEntityKey(entitySetNamecurrent);
              var validatingKey = context.CreateEntityKey(entitySetNamevalidationContext.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.MemberName });
      }
    }
#endif   } }
 

As soon as you will be trying to call DbContext.SaveChanges in the DomainService, if one of the entities is invalidated by this attribute, the save will fail.
Since I didn't test this attribute in the LinqToEntitiesDomainService, I'm affraid that since the ObjectContext doesn't validate entities when submitting changes, you'll have to make a use of the ValidationContext property, please comment if anything is unclear or for further details.
This attribute should work just like all the other ValidationAttributes do.

When using with a Silverlight project and a WCF-RIA service, as soon as you'll try to commit the changes, it will stop and is reported back to the client who has its DataField decorated with the validation error, and since the DomainContext.SubmitChanges in the client is asynchronous, all this will happen asynchronously.

To view raw code please refer here.

Since I didn't invest in testing it in other environments (For instance SL < 5.0) and other test options/features, any comments/improvements will be very appreciated.

What's not implemented is, selecting just the ID column and the duplicate-check column. I was too lazy to invest on building the select column (getting the ID column(s) of a specific entity).
Anyway, depending on the column amount of the table, it shouldn't make such a huge difference, as this query is not supposed to return more than one row anyway, unless bypassed this validation by adding rows directly to database.

Hope this helps,
Shimmy

Comments

# ValidationAttribute that validates a unique field against its fellow rows in the database

Pingback from  ValidationAttribute that validates a unique field against its fellow rows in the database

# re: ValidationAttribute that validates a unique field against its fellow rows in the database

Monday, April 16, 2012 9:23 AM by Evgeni

Code does not work: validationContext.MemberName is null.

MVC 3, EF 4.1

# re: ValidationAttribute that validates a unique field against its fellow rows in the database

Monday, April 16, 2012 1:15 PM by Evgeni

I used code first approach. My model class: http://pastebin.com/SZ7XnXZc

When controller.create is being performing, I get an exception at row

var property = type.GetProperty(validationContext.MemberName);

# re: ValidationAttribute that validates a unique field against its fellow rows in the database

Sunday, May 13, 2012 12:33 AM by Shimmy

@Evgeni, please provide more details on your model-database situation.

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above:
Powered by Community Server (Commercial Edition), by Telligent Systems