1

I got a problem with the use of EF for creating an update request when it comes to fk. I want to modify a navigation field in an existing entity, without loading it before. So the entity is not pre-loaded in the context, and not proxyied;

For the example, i have a simple 1-* relationship which give me this two entities:

public partial class NameMap
{
    public NameMap()
    {
        this.SurnameMaps = new HashSet<SurnameMap>();
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string MainLang { get; set; }

    public virtual ICollection<SurnameMap> SurnameMaps { get; set; }
    public virtual TitleMap TitleMap { get; set; }
}

public partial class TitleMap
{
    public TitleMap()
    {
        this.NameMaps = new HashSet<NameMap>();
    }

    public int Id { get; set; }
    public string Title { get; set; }

    public virtual ICollection<NameMap> NameMaps { get; set; }
}

When i modify some scalar properties, i do the following:

public void PartiallyChangeEntity()
{
    var nmToModify = new NameMap()
    {
        Id = 2, //item i want to change
        Name = "This is a test", //the prop i want to change
        MainLang = String.Empty //must be init and not null - but i come back to this point later (1)
    };

    _context.NameMaps.Attach(nmToModify);

    _context.Entry(nmToModify).Property(a => a.Name).IsModified = true;

    _context.SaveChanges();
}

And EF only modify the Name in the table. (1) No modification are given to the MainLang field, but validation fails if the value is set to null (seems that the fact MainLang field is not nullable in db implies it cant be null when validation is calculated)

Now, i want to modify the Title of a Name. Same as before, i tried:

public void PartiallyChangeEntityConstraint()
{
    var nmToModify = new NameMap()
    {
        Id = 2,
        MainLang = String.Empty //same as (1)
    };

    _context.NameMaps.Attach(nmToModify);

    var title = new TitleMap {Id = 3}

    _context.NameMaps.Attach(title);

    _context.Entry(title).Collection(a => a.NameMaps).CurrentValue.Add(nmToModify);
    _context.Entry(nmToModify).Reference(a => a.TitleMap).CurrentValue = tm;

    _context.SaveChanges();
}

This method failed and throw this error:

Test Name:  PartiallyChangeEntityConstraint
Test FullName:  EFTests.UnitTest1.PartiallyChangeEntityConstraint
Test Source:    d:\TFS\EFTests\EFTests\UnitTest1.cs : line 100
Test Outcome:   Failed
Test Duration:  0:00:02.0409469

Result Message: 
Test method EFTests.UnitTest1.PartiallyChangeEntityConstraint threw exception: 
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.Entity.Core.UpdateException: A relationship from the 'TitleMapNameMap' AssociationSet is in the 'Added' state. Given multiplicity constraints, a corresponding 'NameMap' must also in the 'Added' state.
Result StackTrace:  
at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.RelationshipConstraintValidator.ValidateConstraints()
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.ProduceCommands()
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.<Update>b__2(UpdateTranslator ut)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update[T](T noChangesResult, Func`2 updateFunction)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update()
   at System.Data.Entity.Core.Objects.ObjectContext.<SaveChangesToStore>b__35()
   at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(SaveOptions options, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass2a.<SaveChangesInternal>b__27()
   at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func`1 operation)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(SaveOptions options)
   at System.Data.Entity.Internal.InternalContext.SaveChanges()
 --- End of inner exception stack trace ---
    at System.Data.Entity.Internal.InternalContext.SaveChanges()
   at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
   at System.Data.Entity.DbContext.SaveChanges()
   at EFTests.UnitTest1.PartiallyChangeEntityConstraint() in d:\TFS\EFTests\EFTests\UnitTest1.cs:line 103

I tried lot of idea, like:

_context.Entry(nmToModify).Property(a => a.TitleMap).IsModified = true;

but TitleMap is not a property to Ef, so he don't want it; sounds fair.

My main question is: How to solve partial updating when it comes to navigation properties ?

Note: if its possible, i don't want to expose TitleMapId in NameMap.

Bonus question: is there a way to not have to initialise the not null fields, as i show it in (1)

Thanks for your help.

nanderlini
  • 36
  • 4

1 Answers1

0

This part of the exception gives a little clue what the problem might be:

A relationship from the 'TitleMapNameMap' AssociationSet is in the 'Added' state. Given multiplicity constraints, a corresponding 'NameMap' must also in the 'Added' state.

Your relationship is required, i.e. a NameMap must have a reference to a TitleMap. If a required relationship is added the corresponding dependent entity (NameMap in this case) must be new (that is in Added state as well) because an existing entity already must have a relationship and the relationship could only be changed. EF models changed relationship as deleting the old relationship and adding the new relationship in the relationship manager. Because there is no relationship in state Deleted it assumes that the dependent entity can only be new. However, there is no new entity in the change tracker which causes the exception.

Now, it's likely that the problem could also be solved if the relationship manager would find a relationship entry in state Deleted corresponding to the one that is Added. You can achieve that be giving the NameMap a reference to a TitleMap before you attach:

public void PartiallyChangeEntityConstraint()
{
    var nmToModify = new NameMap()
    {
        Id = 2,
        MainLang = String.Empty,
        TitleMap = new TitleMap { Id = XXX }
    };
    _context.NameMaps.Attach(nmToModify);

    var title = new TitleMap { Id = 3 }
    _context.TitleMaps.Attach(title);

    _context.Entry(nmToModify).Reference(a => a.TitleMap).CurrentValue = title;
    // You could also simply use here: nmToModify.TitleMap = title;
    // or did you disable automatic change detection?

    _context.SaveChanges();
}

EF recognizes assigning the new title to the nmToModify entity as a relationship change from foreign key XXX to 3 and will create two relationship entries in state Deleted and in state Added respectively.

Now, the big question is: What is the value of XXX? It must not be 3 because then you'll get an exception that two objects with the same key have been attached which is forbidden. This problem is easy to circumvent (use 3+1 or something). The bigger problem is that you apparently can't use some arbitrary dummy value (like -1 or 0 or anything else). It must be the foreign key to TitleMap of the NameMap record 2 that is currently actually stored in the database. Otherwise the UPDATE in the database doesn't work because the generated SQL contains a WHERE clause not only for the PK of NameMap but also for the FK to TitleMap (WHERE NameMaps.Id = 2 AND NameMaps.TitleMap_Id = XXX). If no record to update is found (and it won't be found if XXX is not the correct current FK) you get a concurrency exception.

This kind of changing a relationship without knowing the old foreign key is much easier with foreign key associations, i.e. by exposing the FK as a property in your model. Changing the relationship is then just modifying a scalar property.

About avoiding validation exceptions of properties that haven't been changed: You must disable the global context validation and then validate manually at property level (only the properties you actually want to flag as modified):

_context.Configuration.ValidateOnSaveEnabled = false;

var entry = ctx.Entry(nmToModify);
var validationErrors = entry.Property(a => a.Name).GetValidationErrors();
if (validationErrors.Count > 0)
    throw new DbEntityValidationException("An entity property is invalid.", 
        new DbEntityValidationResult[] { new DbEntityValidationResult(
            entry, validationErrors) });

entry.Property(a => a.Name).IsModified = true;

(Idea and code based on this answer which also contains more explanations and details.)

Community
  • 1
  • 1
Slauma
  • 175,098
  • 59
  • 401
  • 420
  • Thanks a lot for this. I get the right sql request now. I must admit that recovering the fk id is a little boring; i'm thinking about exposing fk Id's as scalar property in my pocos. Sounds like it is the best practice to achieve my goal. – nanderlini May 19 '14 at 12:31