Validate a Unique Constraint at dbContext ValidateEntity in Entity Framework






4.57/5 (12 votes)
Validated the unique constraints at dbContext ValidateEntity in a generic way by reading the IndexAttribute values.
Introduction
In "Entity Framework code first", you have the ability to add IndexAttribute to generate unique constraints and indexers for your database and the DB migrations will know how to read these attributes to generate the migration for you.
However, this attribute does NOT do anything related to validate the entities before save changes. so you have to do this check by yourself or get an exception from the database and handle it.
I'm trying to add a generic way to do this validation automatically before dbContext.
Savechanges
()
.
Background
Just to mention that Entity Framework DbContext
does validations on many levels. I'm not going to mention all of them here just to focus on the idea of this post, but what you need to know is that there is a method in DbContext
called ValidateEntity
that will be called internally from DbContext
when you call dbContext.
Savechanges
()
and it will validate those changes that you made before doing the actual work of saving changes.
Our work will be in this ValidateEntity
by overriding it and inject our check for unique constraints for the passed entities.
Using the Code
Consider that you have this simple model that is representing your friends.
public class Friend
{
[Key]
public Guid Id { get; set; }
[Required]
[StringLength(255)]
[Index("UQ_Friend_Name", IsUnique = true)]
public string Name { get; set; }
[Required]
[StringLength(15)]
[Index("UQ_Friend_Code", IsUnique = true)]
public string Code { get; set; }
}
As you see, we have two constraints here to prevent duplication of Name
and Code
. Then, we'll have a very simple DbContext
class like this:
public class FriendsDbContext : DbContext
{
public FriendsDbContext() : base("FriendsSuiteDb")
{
}
public virtual DbSet<Friend> Friends{ get; set; }
}
Now what we're going to do is override the ValidateEntity
method and check for duplications in the database and if we found any duplication, we'll add DbValidationError
to the DbEntityValidationResult
and return this result. Then, the DbContext
will call our method before SaveChanges
and if it found any error, will stop the operation and throw an exception containing our validation errors.
I'm not good at talking, so let's take a look at the code:
Add this method to your dbContext
:
protected override DbEntityValidationResult ValidateEntity
(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
// create our customized result to add a possible DbValidationError to it
var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
// We need to check for duplication just in case of adding new entities or modifing existed ones
if (entityEntry.State == EntityState.Added || entityEntry.State == EntityState.Modified)
checkDuplication(entityEntry, result);
// After we did our check to the entity, if we found any duplication, we don't want to continue
// so we just return our DbEntityValidationResult
if (!result.IsValid)
return result;
// If we didn't find and duplications, then let DbContext do its ordinary checks
return base.ValidateEntity(entityEntry, items);
}
As described in the comments above the code, this is the logic of validation. All of that I guess is normal for most of you. The real work will be in the checkDuplication
method.
The Important Work Is Here
So let's add checkDuplication
method:
private void checkDuplication(DbEntityEntry entityEntry, DbEntityValidationResult result)
{
//we'll get the entity that we want to check from the passed DbEntityEntry
var entity = entityEntry.Entity;
// Using reflection to retrive all properties that implement IndexAttribute
// We'll have with each property the IndexAttribute(s) that it has
var propertiesDictionary = (from p in entity.GetType().GetProperties()
let attrs = p.GetCustomAttributes(typeof(IndexAttribute), false).Cast<IndexAttribute>()
where attrs.Any(a => a.IsUnique)
select new
{
Property = p,
Attributes = attrs.Where(a => a.IsUnique)
}).ToList();
// Get Distinct list of all unique indexes that we have
var indexNames = propertiesDictionary.SelectMany
(x => x.Attributes).Select(x => x.Name).Distinct();
// iterate through indexes to check db values
foreach (var indexName in indexNames)
{
// We'll get all the properties that related to this unique index
// because one index may have combined properties involved in it
Dictionary<string, PropertyInfo> involvedProperties = propertiesDictionary
.Where(p => p.Attributes.Any(a => a.Name == indexName))
.ToDictionary(p => p.Property.Name, p => p.Property);
// Get the DbSet that is representing the Entity table
DbSet set = Set(entity.GetType());
//Using dynamic linq to query database for existed rows
//with the values of the properties that we passed
var whereClause = "";
var whereParams = new List<object>();
var i = 0;
foreach (var involvedProperty in involvedProperties)
{
if (whereClause.Length > 0)
whereClause += " AND ";
if (Nullable.GetUnderlyingType(involvedProperty.Value.PropertyType) != null)
whereClause += "it." + involvedProperty.Key + ".Value.Equals(@" + i + ")";
else
whereClause += "it." + involvedProperty.Key + ".Equals(@" + i + ")";
whereParams.Add(involvedProperty.Value.GetValue(entity));
i += 1;
}
// If this is an update, then we should exclude the same record from our query,
// then we just need to add new condition to tell search for all records but not this one
if (entityEntry.State == EntityState.Modified)
{
whereClause += " AND (";
var key = this.GetEntityKey(entity);
for (var j = i; j < key.EntityKeyValues.Count() + i; j++)
{
if (j != i)
whereClause += " OR ";
whereClause += "it." + key.EntityKeyValues[j - i].Key + " <> @" + j;
whereParams.Add(key.EntityKeyValues[j - i].Value);
}
whereClause += " )";
}
//If we find any record, we should add DbValidationError with suitable error message
if (set.Where(whereClause, whereParams.ToArray()).Any())
{
// this logic is just to create the error message
var errorMessageBuilder = new StringBuilder()
.Append("The ")
.Append(involvedProperties.Count > 1 ? "values " : "value ")
.Append("of '")
.Append(string.Join(", ", involvedProperties.Keys))
.Append("' ")
.Append(involvedProperties.Count > 1 ? "are " : "is ")
.Append("already exist!.");
//Add the error to the result and return it
result.ValidationErrors.Add(new DbValidationError(indexName, errorMessageBuilder.ToString()));
}
}
}
As you noticed probably I'm using the "Linq Dynamic Query Library" to generate the query that responsible of checking the database values.
To have good isolation, I just isolated the code of getting the EntityKey
to an ExtentionMethod
. It's simple like this:
public static EntityKey GetEntityKey(this DbContext context, object entity)
{
var objectContext = ((IObjectContextAdapter) context).ObjectContext;
var setName = getObjectSetName(objectContext, entity.GetType());
return objectContext.CreateEntityKey(setName, entity);
}
private static string getObjectSetName(ObjectContext oc, Type entityType)
{
var createObjectSetMethodInfo = typeof (ObjectContext)
.GetMethods()
.Single(i => i.Name == "CreateObjectSet" && !i.GetParameters().Any());
var objectSetType = Assembly.GetAssembly(typeof (ObjectContext))
.GetTypes()
.Single(t => t.Name == "ObjectSet`1");
var objectSet = createObjectSetMethodInfo.MakeGenericMethod(entityType).Invoke(oc, null);
var pi = objectSetType.MakeGenericType(entityType).GetProperty("EntitySet");
var entitySet = pi.GetValue(objectSet) as EntitySet;
return entitySet.Name;
}
That's it! Hope this tip helps you.
Sample code on GitHub