MVC Validation 2: Validating for column uniqueness

I find a common problem is handling the case of a column needing to be unique and how to handle this validation scenario. It’s easy enough to set a constraint in the backing SQL server, but handling this by catching an exception and then transferring this to the model state seems like a bad solution. What we want is to present a somewhat generic solution to this problem and end up with something like this:

class MyEntity {
    int Id { get; set; }
    
    [Unique(ErrorMessage="Name must be unique")]
    string Name { get; set; }
}

I want this working out of the box without having to do anything at all in the validation page. This presents a few problems. Mainly the issue of how to get the database context to the UniqueAttribute class that is the source of our [Unique] attribute, and how to construct these queries.

For the first problem there are a few choices, you can either set a ValidationContext manually and add your context as a dictionary parameter in the items field of the ValidationContext constructor. This solution is problematic as no of the built-in validation methods that gets run my MVC and Entity framework will provide that parameter. So that solution is out.

The second solution is to use the IServiceProvider interface, which is kinda sorta an inversion of control container. However, I found this to be a bit inflexible, and without using IServiceProvider to wrap a real dependency injection framework, which according to some is an anti-pattern, we’d end up with a new DbContextfor each instance.

So, using the solution presented earlier, this is what I came up with:

using System;
using System.ComponentModel.DataAnnotations;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class UniqueAttribute : ValidationAttribute
{
	public String[] AdditionalMembers { get; set; }

	protected override ValidationResult IsValid(object value, ValidationContext context)
	{
		var uniqueValidator = IoC.Get<IUniqueValidator>();
		if (uniqueValidator == null)
		{
			throw new ValidationException(
				"No IUniqueValidator found in UniqueAttribute. Cannot determine uniqueness");
		}

		// MemberName may not be intialized when this is called. It will be later on
		if (context.MemberName != null)
		{
			if (!uniqueValidator.CanPropertyBeUniquelyStored(context.ObjectInstance, context.MemberName, AdditionalMembers))
			{
				return new ValidationResult(ErrorMessage, new[] {context.MemberName});
			}
		}

		return ValidationResult.Success;
	}
}

A few comments on this, it uses the IoC wrapper class described in a previous post. If you’re using a dependency injection framework out of the box, substitute this with whatever Kernel call you need to use. This in turn returns an interface that does the actual column uniqueness check. It would have been preferable to use constructor injection, but this won’t play nice with how .NET instantiates the attribute classes.

I’ve also included an AdditionalMembers to handle compound unique members. The member you attach the attribute to will get the validation message, the others will be validated along with the annotated member. Lets have a look at the interface.

using System;

public interface IUniqueValidator
{
	bool CanPropertyBeUniquelyStored(object entity, string propName, string[] additionalMembers);
}

No big deal, pass the entity, the property name and any additional members you want validated along with the member. Now for the implementation of this interface.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Dynamic;

internal class UniqueValidator : IUniqueValidator
{
	private readonly IDbContext dbContext;

	public UniqueValidator(IDbContext dbContext)
	{
		this.dbContext = dbContext;
	}

	public bool CanPropertyBeUniquelyStored(object entity, string propName, string[] additionalMembers)
	{
		var entityType = entity.GetType();
		DbSet dbSet = dbContext.Set(entityType);

		var uniqueProperties = new List<string> {propName};
		if (additionalMembers != null)
		{
			uniqueProperties.AddRange(additionalMembers);
		}
		int propertyIndex = 0;
		string query = string.Join(" AND ", from a in uniqueProperties select GenerateQuery(a, entityType, propertyIndex++));
		object[] values = (from a in uniqueProperties select GetValue(a, entity, entityType)).ToArray();

		IQueryable list = dbSet.Where(query, values);

		var idProp = entityType.GetProperty("Id");
		var entityId = idProp.GetValue(entity, null);

		// Iterate through the list, if there is any entity which does not have the same ID 
		// as the one we are trying to insert, this property is not unique
		foreach (var listEntity in list)
		{
			var listEntityId = idProp.GetValue(listEntity, null);
			if (!listEntityId.Equals(entityId))
			{
				return false;
			}
		}

		return true;
	}

	private object GetValue(string propName, object entity, Type entityType)
	{
		var propertyInfo = entityType.GetProperty(propName);
		var propType = propertyInfo.PropertyType.Name;
		return propertyInfo.GetValue(entity, null);
	}

	private string GenerateQuery(string propName, Type entityType, int propertyIndex)
	{
		var propertyInfo = entityType.GetProperty(propName);
		var propType = propertyInfo.PropertyType.Name;            

		return propName + " == @" + propertyIndex;
	}
}

Again, we rely upon dependency injection to inject a IDbContext interface to the constructor. This is a simple interface derived from the Entity framework derived DbContext that provides us with at least the Set(Type entityType) method to get our hands on a DbSet. To construct the queries we use DynamicQuery, a smallish library that allows us to construct queries based on string constructs. This particular code relies on the primary key being called Id and being an Integer.

So, in all, the solution is easy enough if you have a working dependency injection framework. And if you don’t it should be easy enough to modify into using the MS IServiceProvider interface instead and creating a real DbContext instead of getting a derived interface injected. No reason to write custom validation outside of attributes either way.

Advertisements
Tagged , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: