UniqueAttribute that validates a unique field against its fellow rows in the database (inherits DataAnnotations.ValidationAttribute)

23/01/2012

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

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

6 comments

  1. Evgeni16/04/2012 ב 09:23

    Code does not work: validationContext.MemberName is null.
    MVC 3, EF 4.1

    Reply
    1. Brian21/12/2014 ב 14:32

      @Evgeni, adding this worked for me:

      if (validationContext.MemberName == null) validationContext.MemberName = validationContext.DisplayName;

      Reply
  2. Evgeni16/04/2012 ב 13:15

    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);

    Reply
  3. Shimmy13/05/2012 ב 00:33

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

    Reply
  4. Goran29/06/2012 ב 17:23

    Hi,

    what if I am doing batch updates, then I could have 2 rows in memory that have duplicate values. If you are just comparing it to database records, then it will fail on 2nd row insert.

    Also, do you know of any example how to validate uniqueness of composite unique key (composed of two columns which are FKs)?

    Reply
  5. Shimmy30/06/2012 ב 21:47

    @Goran:
    1) I didn’t test it on batch inserts, but I believe there might be a chance that once the validation failed in the batch, it throws back to the client, without further processing. If I’m wrong you could override one of the validation methods in your WCF domain-service and make sure if a validation attribute is caught, don’t continue the validation – just stop the entire transaction.
    2) Didn’t have the chance to work on composite unique keys – of course you could decorate both the unique fields with the UniqueAttribute individually, but this of course will validate them separately.
    I would suggest you to create a entity-level ValidationAttribute that walks thru all the fields that are unique (specifying manually or with new attributes), and validating them all at once.

    HTH,
    Shimmy

    Reply