3

I have overridden my db.SaveChanges() so I can call my FluentValidation validators before it actually attempts to save it.

I have a validator for each entity marked with IValidatableEntity and if the entity matches it will call it and pass the objectStateEntry in.

public virtual IEnumerable<string> SaveChanges(User user)
{
     List<string> validationErrors = new List<string>();
     if (this.Configuration.ValidateOnSaveEnabled)
     {
         foreach (var entry in ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager
              .GetObjectStateEntries(System.Data.Entity.EntityState.Added | System.Data.Entity.EntityState.Deleted | System.Data.Entity.EntityState.Modified | System.Data.Entity.EntityState.Unchanged)
              .Where(entry => (entry.Entity is IValidatableEntity)))
              {
                  validationErrors.AddRange(((IValidatableEntity)entry.Entity).Validate(entry));
              }
         }

    if (!validationErrors.Any())
    { .....

The problem I have is that I get two different behaviours depending on how I add the object to the dbContext. I Guess because it only marks the aggregate root as being modified and only gives it an entry?

// Example A - Calls the Organisation Validator Only
 organisation.Client.Add(client); 

// Example B - Calls the Client Validator - which is correct
db.Client.Add(client);

Is there anyway to get EF automatically detect child properties have changed (Add / Modified) and call them? It kind of breaks my validation model if it doesn't, I was banking on updating the aggregate root and having EF call the necessary child validations as they should have unique entries.

Do I have to chain validators inside my Fluent Validations to catch these? I Didn't want a case of where my fluent Validator will have to check potentially hundreds of child entities. (some contain db lookups etc).

Thanks

Peter Lea
  • 1,731
  • 2
  • 15
  • 24

2 Answers2

2

Try to call DetectChanges at the beginning of your overridden SaveChanges method (it must be before you call GetObjectStateEntries):

this.ChangeTracker.DetectChanges();

The difference between the two lines to add a Client is that organisation.Client.Add(client) does not call any EF code directly (it's just adding an item to a collection in a POCO) while db.Client.Add(client) does and the DbSet<T>.Add method will call change detection automatically to update the entity states.

In the first case if you don't call any EF method before SaveChanges the base.SaveChanges will detect the changes as the very last place to ensure that all entity states are correct and all changes are saved. But base.SaveChanges is too late for the code in your overridden SaveChanges because it is after your evaluation of GetObjectStateEntries. At that point the entity state of the added client could still be Detached (i.e. not existing in the state manager) instead of Added. In order to fix this you have to call DetectChanges manually early enough to retrieve the final entity states in GetObjectStateEntries.

Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Brilliant thanks slauma that all makes sense. You both gave great answers I cant tell who posted first. But as ken mentioned your post ill assume you did first. – Peter Lea Feb 10 '14 at 22:07
  • @PeterLea: You can always see an exact timestamp when you hover with your mouse over that "answered *x hours ago*" text below the answers. – Slauma Feb 11 '14 at 01:15
1

I assume organisation is a simple POCO, so the following code:

organisation.Client.Add(client);

just adds another POCO in an ICollection of POCOs. EF has no way to detect you are adding an entity to the context.

In the other hand, the following code:

db.Client.Add(client);

adds a POCO directly in an implementation of ICollection (DbCollectionEntry) that is related to Entity Framework and is in charge of what's called change tracking (among other things). This is possible thanks to dynamic proxy types generated at runtime (see https://stackoverflow.com/a/14321968/870604).

So you'll have to detect changes manually (see @Slauma answer). Another option would be to use a proxy object instead of your organisation POCO. This would be possible by calling:

var newOrganisation = dbContext.Set<Organisation>().Create();

The above code of course works for a new organisation instance.

Community
  • 1
  • 1
ken2k
  • 48,145
  • 10
  • 116
  • 176
  • Thanks ken. Brilliant answer I wish I could share the bounty. Sorry if I got it wrong and you answered first! – Peter Lea Feb 10 '14 at 22:14