Nomenator
Nomenator

Reputation: 1137

How to validate entity against the context in Entity Framework?

I usually validate entities in Entity Framework by calling entity.IsValid() and creating appropriate ValidationAttribute class for the entity.

Now, however, I run into a case when I need to validate an entity not just on its own, but within the context to which it belongs, as an example:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CourseValidation : ValidationAttribute
{
    public CourseValidation() {}
    protected override ValidationResult IsValid (object value, ValidationContext validationContext)
    {
        List<string> messages = new List<string>();
        if (value is Course)
        {
            Course course = (Course)value;
            if (course.Context != null)
            {
                if (course.Context.Courses.Any(c => c.Name == course.Name && c.Department.ID == course.Department.ID))
                {
                    messages.Add($"Cannot create a course with name {course.Name} in {course.Department.Name} department because a course with this name already exists in this department.");
                }
            }
            else messages.Add("Course is being improperly handled by the software, please contact support department");
         }
         else messages.Add("Course is expected, but does not exist");

         if (messages.Count > 0) return new ValidationResult(string.Join(Environment.NewLine, messages));
         else return ValidationResult.Success;
     }
}

There is a difficulty: simply using context.Courses.Add(course) does not cause context.Courses.Where(c => c.Name == course.Name) to return anything. Instead, it will require context.SaveChanges() before the entity is available as part of the entire collection. Which means that I will not be able to validate the entity against the collection before trying to save it to database.

I know this example is simplistic and can be handled by a database-side unique constraint, but even if we're not going to look at more complex examples, I see a good reason to filter the invalid entries before trying to commit them to the database (because if one entry in the transaction offends a constraint, the entire transaction will be blocked), and to put all validation criteria in a single place, instead of splitting them between different classes and/or database schema (to maintain single responsibility).

In which ways can a validation strategy be implemented to satisfy these requirements?

Upvotes: 2

Views: 2449

Answers (1)

Steve Greene
Steve Greene

Reputation: 12314

What we do is override the context's ValidateEntity method. This gives you a chance to check things in the database (like duplicates, etc) before committing the changes.

Simply add the overriden class to your context and do any checks you need:

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
    IDictionary<object, object> items)
{

    if (entityEntry.Entity is Course &&
                (entityEntry.State == EntityState.Added 
                  || entityEntry.State == EntityState.Modified))
    {
        var courseToCheck = ((Course)entityEntry.Entity);

        //check for uniqueness 
        if (Courses.Any(c => c.Name == course.Name && c.Department.ID == course.Department.ID)))
            return 
                   new DbEntityValidationResult(entityEntry,
                 new List<DbValidationError>
                     {
                         new DbValidationError( "Name",
                             $"Cannot create a course with name {courseToCheck .Name} in {courseToCheck .Department.Name} department because a course with this name already exists in this department.")
                     });
    }

    return base.ValidateEntity(entityEntry, items);
}

Now you can call context.GetValidationErrors() and deal with the error(s) before saving. For example here.

Upvotes: 2

Related Questions