Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unique constraint with data annotation

I'm using the System.ComponentModel.DataAnnotations namespace to validate my domain classes. How can I create a custom attribute to validate the uniqueness of a property regardless of the database (through some interface, for example)?

like image 636
Murilo Lima Avatar asked Aug 04 '10 14:08

Murilo Lima


2 Answers

This is the solution I came up with for this situation, it simply checks the table for a record with a different id that has the same value for the property being validated. It assumes that you will be using LinqToSQL, and that any table on which this kind of validation is required has a single ID column.

I'd also put a unique constraint on the underlying table in the database. This attribute allows me to put a nice error message on the form and associate it with the appropriate property.

public class UniqueAttribute : ValidationAttribute
{
    public Func<DataContext> GetDataContext { get; private set; }
    public string IDProperty { get; private set; }
    public string Message { get; private set; }

    public UniqueAttribute(Type dataContextType, string idProperty, string message)
    {
        IDProperty = idProperty;
        Message = message;
        GetDataContext = () => (DataContext)Activator.CreateInstance(dataContextType);
    }

    public UniqueAttribute(Type dataContextType, string idProperty, string message, string connectionString)
    {
        IDProperty = idProperty;
        Message = message;
        GetDataContext = () => (DataContext)Activator.CreateInstance(dataContextType, new object[] { connectionString });
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var idProperty = validationContext.ObjectType.GetProperty(IDProperty);
        var idType = idProperty.PropertyType;
        var id = idProperty.GetValue(validationContext.ObjectInstance, null);

        // Unsightly hack due to validationContext.MemberName being null :(
        var memberName = validationContext.ObjectType.GetProperties()
            .Where(p => p.GetCustomAttributes(false).OfType<DisplayAttribute>().Any(a => a.Name == validationContext.DisplayName))
            .Select(p => p.Name)
            .FirstOrDefault();
        if (string.IsNullOrEmpty(memberName))
        {
            memberName = validationContext.DisplayName;
        }
        // End of hack

        var validateeProperty = validationContext.ObjectType.GetProperty(memberName);
        var validateeType = validateeProperty.PropertyType;
        var validatee = validateeProperty.GetValue(validationContext.ObjectInstance, null);

        var idParameter = Expression.Constant(id, idType);
        var validateeParameter = Expression.Constant(validatee, validateeType);
        var objectParameter = Expression.Parameter(validationContext.ObjectType, "o");
        var objectIDProperty = Expression.Property(objectParameter, idProperty);
        var objectValidateeProperty = Expression.Property(objectParameter, validateeProperty);
        var idCheck = Expression.NotEqual(objectIDProperty, idParameter);
        var validateeCheck = Expression.Equal(objectValidateeProperty, validateeParameter);
        var compositeCheck = Expression.And(idCheck, validateeCheck);
        var lambda = Expression.Lambda(compositeCheck, objectParameter);
        var countMethod = typeof(Queryable).GetMethods().Single(m => m.Name == "Count" && m.GetParameters().Length == 2);
        var genericCountMethod = countMethod.MakeGenericMethod(validationContext.ObjectType);

        using (var context = GetDataContext())
        {
            var table = context.GetTable(validationContext.ObjectType) as IQueryable<Models.Group>;
            var count = (int)genericCountMethod.Invoke(null, new object[] { table, lambda });
            if (count > 0)
            {
                return new ValidationResult(Message);
            }
        }

        return null;
    }
}

Example usage:

[MetadataType(typeof(UserMetadata))]
public partial class Group : IDatabaseRecord
{
    public class UserMetadata
    {
        [Required(ErrorMessage = "Name is required")]
        [StringLength(255, ErrorMessage = "Name must be under 255 characters")]
        [Unique(typeof(MyDataContext), "GroupID", "Name must be unique")]
        public string Name { get; set; }
    }
}
like image 200
daveb Avatar answered Oct 11 '22 21:10

daveb


just do something like this on your model

[StringLength(100)]
[Index("IX_EntidadCodigoHabilitacion", IsUnique = true)]
public string CodigoHabilitacion { get; set; }
like image 43
Van Mart Avatar answered Oct 11 '22 22:10

Van Mart