Entity Framework validation with partial updates
Asked Answered
W

3

23

I'm using Entity Framework 5.0 with DbContext and POCO entities. There's a simple entity containing 3 properties:

public class Record
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsActive { get; set; }
}

The Title field is always unmodified, and the UI simply displays it without providing any input box to modify it. That's why the Title field is set to null when the form is sent to the server.

Here's how I tell EF to perform partial update of the entity (IsActive field only):

public class EFRepository<TEntity>
{
   ...
   public void PartialUpdate(TEntity entity, params Expression<Func<TEntity, object>>[] propsToUpdate)
   {
       dbSet.Attach(entity);
       var entry = _dbContext.Entry(entity);
       foreach(var prop in propsToUpdate)
           contextEntry.Property(prop).IsModified = true;
   }
}

and the call:

repository.PartialUpdate(updatedRecord, r => r.IsActive);

Calling SaveChanges method, I get the DbEntityValidationException, that tells me, Title is required. When I set dbContext.Configuration.ValidateOnSaveEnabled = false, everything is OK. Is there any way to avoid disabling validation on the whole context and to tell EF not to validate properties that are not being updated? Thanks in advance.

Woodcock answered 13/10, 2012 at 9:41 Comment(10)
This seems like a lot of work to do something that is quite simple to deal with otherwise. You just include a hidden field on the form with your read-only model items, then they are included in the update and EF does its change tracking and knows the value hasn't changed.Noonberg
What about stub entities than? For example, I' ve an Action method that marks an entity as deleted. The following code: var person = new Person { Id = 5 }; dbSet.Attach(person); dbSet.Entry(person).Property(p => p.IsDeleted).IsModified = true; dbContext.SaveChanges(); will cause the same exception. Does DbContext validation work well with stub entities at all? I'd like to aviod retrieving the whole entity from database just for marking it as deleted.Woodcock
Does it really work? Attach is used when you have an entity in hand and you are sure that this entity exists in the database and is identical. You attach an entity that is different from what in the store (the title is different). Then you mark the entity as modified (by marking a property to be modified). Since EF operates on entities and not on properties it will update all the properties and not only the one that is marked as modified. My mental model of the EF tells me that the Title column in the database will be set to null after this. Can you check if this is not the case?Conformance
Validation by default validates all properties of entities in Added and Modified state. It is possible to change this behavior by using extension mechanisms exposed by validation. Note that once you mark your attached entity as modified it's no longer a stub entity as I believe it will be sent to the database...Conformance
EF allows partial updates of the entity. For example, dbSet.Attach(entity); dbContext.Entry(entity).State = EntityState.Modified; dbContext.SaveChanges(); will update the whole entity. If you tell EF explicitly, what properties to update, only these properties are updated. dbSet.Attach(entity); dbContext.Entry(entity).Property(e => e.Title).IsModified = true; dbContext.SaveChanges(); will update Title only. With disabled validation, this works well.Woodcock
With this approach(explicitly telling EF what properties to update) I even can update another property (entity.IsActive = false), but so far I do not mark it as updated (e => e.IsActive).IsModified = true; it will not be updated. entity.State = EntityState.Modified marks the whole entity as updated at once.Woodcock
@Woodcock - You don't have to retrieve the entity to mark it as deleted, just issue a simple SQL statement using dbSet.SqlCommand("UPDATE entity SET deleted = true where x = y"), this is MUCH simpler than the path you are taking.Noonberg
@Woodcock - I think you're over-engineering a solution. It may work today, but AFAIK this is not a supported way to do this, which means it may break in a future version. The fact that you are having troubles should tell you that it's not, in fact, actually working. Stub entities are supported, but you can only the key value is supported. If you have more than just the key, it's not a stub anymore.Noonberg
@Woodcock - BTW, stub entities are no longer the preferred method, rather FK Associations are. And they're more powerful.Noonberg
Did not think about raw sql :) Thanks for your responses, guysWoodcock
P
25

If you use partial updates or stub entities (both approaches are pretty valid!) you cannot use global EF validation because it doesn't respect your partial changes - it always validates whole entity. With default validation logic you must turn it off by calling mentioned:

dbContext.Configuration.ValidateOnSaveEnabled = false

And validate every updated property separately. This should hopefully do the magic but I didn't try it because I don't use EF validation at all:

foreach(var prop in propsToUpdate) {
    var errors = contextEntry.Property(prop).GetValidationErrors();
    if (erros.Count == 0) {
        contextEntry.Property(prop).IsModified = true;
    } else {
        ...
    }
}

If you want to go step further you can try overriding ValidateEntity in your context and reimplement validation in the way that it validates whole entity or only selected properties based on state of the entity and IsModified state of properties - that will allow you using EF validation with partial updates and stub entities.

Validation in EF is IMHO wrong concept - it introduces additional logic into data access layer where the logic doesn't belong to. It is mostly based on the idea that you always work with whole entity or even with whole entity graph if you place required validation rules on navigation properties. Once you violate this approach you will always find that single fixed set of validation rules hardcoded to your entities is not sufficient.

One of things I have in my very long backlog is to investigate how validation affects speed of SaveChanges operation - I used to have my own validation API in EF4 (prior to EF4.1) based on DataAnnotations and their Validator class and I stopped using it quite soon due to very poor performance.

Workaround with using native SQL has same effect as using stub entities or partial updates with turned off validation = your entities are still not validated but in addition your changes are not part of same unit of work.

Philtre answered 14/10, 2012 at 8:0 Comment(9)
Even when you disable entity validation not everything is fine. For example I have properties which shall have an unchanged value but when I do not set an explicit value these props get all implicitly default values like 0 for an integer. The result is to make it really work you have to use RAW Sql there is no workaround or solution for this scenario by EF 6.xClipboard
@Pascal: If they have unchanged value you should either have that value filled (so default value will not be used) or you should use partial updates and don't pass them in updates to database.Philtre
Option1: I do not want to have that value filled because then I have to go to the database again for every entity I have to update Option2: I do not understand can you show please a little code? or explan more? I used partial update with property.IsModified = true; what more is missing here?Clipboard
@Pascal: I meant don't mark those properties as modified if you don't want to send those default values back to database.Philtre
But I do NOT mark those properties as modified. I just set ONE property.IsModified = true not more.Clipboard
@Pascal: Well I probably misunderstood your problem. So your other properties are set to default values - but only in application (not in database), is it correct? Are those properties nullable? If not they must have value so you either need to set that value explicitly or default value will be used.Philtre
Those properties are all NOT NULL in database. The one property which I modify and update is NULL. The content property can have text but need not to have text. Now you say I have to set the value explicitly but that would mean I have to get data from the database which I have not available on the Update operation. That is stupid from EF. Then I better stick with my RAW SQL 2-liner... Thanks for your time.Clipboard
I've added an example that filters the validation to the properties that are IsModified only.Signac
So we switch off validation on the context and leave the view models to provide it. That's a fairly significant tinkering with EF!Sackman
S
20

In reference to Ladislav's answer, I've added this to the DbContext class, and it now removes all the properties that aren't modified.
I know its not completely skipping the validation for those properties but rather just omitting it, but EF validates per entity not property, and rewriting the entire validation process anew was too much of hassle for me.

protected override DbEntityValidationResult ValidateEntity(
  DbEntityEntry entityEntry,
  IDictionary<object, object> items)
{
  var result = base.ValidateEntity(entityEntry, items);
  var falseErrors = result.ValidationErrors
    .Where(error =>
    {
      if (entityEntry.State != EntityState.Modified) return false;
      var member = entityEntry.Member(error.PropertyName);
      var property = member as DbPropertyEntry;
      if (property != null)
        return !property.IsModified;
      else
        return false;//not false err;
    });

  foreach (var error in falseErrors.ToArray())
    result.ValidationErrors.Remove(error);
  return result;
}
Signac answered 17/4, 2015 at 3:21 Comment(4)
thank you! this is the most elegant way I found, but I would suggest also to add this condition if (entityEntry.State != EntityState.Modified) return false; inside the Where, to be sure to remove the falseErrors only when updating the entity, otherwise when adding the validation will be all removed, just because the flag IsModified is not set.Lorislorita
I simply added a if (entityEntry.State == EntityState.Modified) wrapping the var falseErrors and the foreach to prevent the behaviour Michael Denny described. Better than do it in the Linq's query.Balneal
Hey Shimmy, cheers for this snippet. Wondering if you know if this is still required at EF6.1? I just had a check, and EF6 was out by 2013, but it seems like something that could've since been improved on the EF side since last April?Cawley
@MichaelDenny I added that. I don't use this anymore, so I don't know if it's gonna work.Signac
L
3

This is a remix of previous @Shimmy response and it's a version that I currently use.

What I've added is the clause (entityEntry.State != EntityState.Modified) return false; in the Where:

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    var result = base.ValidateEntity(entityEntry, items);

    var falseErrors = result
        .ValidationErrors
        .Where(error =>
        {
            if (entityEntry.State != EntityState.Modified) return false;
            var member = entityEntry.Member(error.PropertyName);
            var property = member as DbPropertyEntry;
            if (property != null) return !property.IsModified;
            return false;
        });

    foreach (var error in falseErrors.ToArray())
    {
        result.ValidationErrors.Remove(error);
    }

    return result;
}
Lorislorita answered 23/3, 2017 at 9:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.