Entity Framework Code First Fluent Validation
Back to Entity Framework Code First (EFCF) validation. On my previous post I mentioned that EFCF did not support fluent validation. While this is true, it isn’t too hard to implement one such mechanism, which is exactly why I am writing this!
I will be using the SavingChanges event to inject the validation logic, which will be implemented by strongly typed delegates. Let’s see some code:
1: public static class DbContextExtensions
2: {
3: private static IDictionary<Type, Tuple<Delegate, String>> entityValidations = new ConcurrentDictionary<Type, Tuple<Delegate, String>>();
4:
5: public static void AddEntityValidation<TEntity>(this DbContext context, Func<TEntity, Boolean> validation, String message) where TEntity : class
6: {
7: if (context == null)
8: {
9: throw new ArgumentNullException("context");
10: }
11:
12: if (validation == null)
13: {
14: throw new ArgumentNullException("validation");
15: }
16:
17: if (String.IsNullOrWhiteSpace(message) == true)
18: {
19: throw new ArgumentNullException("message");
20: }
21:
22: if (entityValidations.ContainsKey(typeof(TEntity)) == false)
23: {
24: (context as IObjectContextAdapter).ObjectContext.SavingChanges += delegate
25: {
26: if (context.Configuration.ValidateOnSaveEnabled == true)
27: {
28: IEnumerable<TEntity> entities = context.ChangeTracker.Entries<TEntity>().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified).Select(x => x.Entity).ToList();
29:
30: foreach (TEntity entity in entities)
31: {
32: String error = ValidateEntity(entity);
33:
34: if (String.IsNullOrWhiteSpace(error) == false)
35: {
36: throw (new ValidationException(error));
37: }
38: }
39: }
40: };
41: }
42:
43: entityValidations[typeof(TEntity)] = new Tuple<Delegate, String>(validation, message);
44: }
45:
46: private static String ValidateEntity<TEntity>(TEntity entity)
47: {
48: Type entityType = typeof(TEntity);
49:
50: if (entityValidations.ContainsKey(entityType) == true)
51: {
52: Tuple<Delegate, String> entry = entityValidations[entityType];
53: Func<TEntity, Boolean> validation = entry.Item1 as Func<TEntity, Boolean>;
54:
55: if (validation(entity) == false)
56: {
57: return (entry.Item2);
58: }
59: }
60:
61: return (null);
62: }
63: }
We have an extension method that allows declaring, for an entity type, a validation expression, such as this:
1: ctx.AddEntityValidation<SomeEntity>(x => x.SomeProperty != null, "SomeProperty is required");
The validation will be fired when the SaveChanges method is called and the errors will be encapsulated in a ValidationException:
1: try
2: {
3: ctx.SaveChanges();
4: }
5: catch (ValidationException ex)
6: {
7: //see content of ex.ValidationResult.ErrorMessage
8: }
This code can certainly be improved – multiple validations per entity, property-based validations, etc – but I think it is good enough to illustrate my technique.
One final note: the fluent validation will only be fired if the ValidateOnSaveEnabled property is set to true, which is the default.