Click here to Skip to main content
14,735,318 members
Articles » Languages » C# » Reflection
Tip/Trick
Posted 20 Oct 2016

Stats

31K views
8 bookmarked

Validate a Unique Constraint at dbContext ValidateEntity in Entity Framework

Rate me:
Please Sign up or sign in to vote.
4.57/5 (12 votes)
8 Nov 2016CPOL
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 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Wahid Bitar
Technical Lead Digital Wink
Turkey Turkey
No Biography provided

Comments and Discussions

 
QuestionVery Helpful Pin
Tomboi8831-May-19 11:09
MemberTomboi8831-May-19 11:09 
AnswerRe: Very Helpful Pin
Tomboi883-Jun-19 9:54
MemberTomboi883-Jun-19 9:54 
QuestionDownload Pin
Member 110514393-Nov-16 5:29
MemberMember 110514393-Nov-16 5:29 
AnswerRe: Download Pin
Wahid Bitar3-Nov-16 6:40
professionalWahid Bitar3-Nov-16 6:40 
QuestionVery helpful article Pin
A.Dabbas20-Oct-16 9:08
MemberA.Dabbas20-Oct-16 9:08 
AnswerRe: Very helpful article Pin
Wahid Bitar20-Oct-16 9:12
professionalWahid Bitar20-Oct-16 9:12 
GeneralRe: Very helpful article Pin
A.Dabbas20-Oct-16 9:21
MemberA.Dabbas20-Oct-16 9:21 
GeneralRe: Very helpful article Pin
Wahid Bitar20-Oct-16 10:07
professionalWahid Bitar20-Oct-16 10:07 
SuggestionLayer is not Important Pin
Khalil Kothia, PMP31-Oct-16 22:54
professionalKhalil Kothia, PMP31-Oct-16 22:54 
PraiseMessage Closed Pin
31-Oct-16 22:51
professionalKhalil Kothia, PMP31-Oct-16 22:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.