Reputation: 1137
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
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