dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.73k stars 3.18k forks source link

CurrentValues.SetValues() not working for nested objects #28531

Closed ScottKane closed 2 years ago

ScottKane commented 2 years ago

I have the following methods on a repository:

public async Task<T> AddAsync(T entity)
{
    await _dbContext.Set<T>().AddAsync(entity);
    return entity;
}

public async Task UpdateAsync(T entity)
{
    var selected = await _dbContext.Set<T>().FindAsync(entity.Id);
    if (selected is not null)
        _dbContext.Entry(selected).CurrentValues.SetValues(entity);
}

I have the following entity:

public abstract class AuditableEntity<TId> : IAuditableEntity<TId>
{
    public TId Id { get; set; }
    public string? CreatedBy { get; set; }
    public DateTime? CreatedOn { get; set; }
    public string? LastModifiedBy { get; set; }
    public DateTime? LastModifiedOn { get; set; }
}

public class Customer : AuditableEntity<int>
{
    public int Title { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
    public Contact Contact { get; set; }
    public int LeadSource { get; set; }
    public IList<Order> Orders { get; set; }
}

public class Address
{
    public string? Number { get; set; }
    public string? Postcode { get; set; }
    public string? Street { get; set; }
    public string? Locality { get; set; }
    public string? Town { get; set; }
    public string? County { get; set; }
    public string? Country { get; set; }
}

public class Contact
{
    public string? Mobile { get; set; }
    public string? Home { get; set; }
    public string Email { get; set; }
}

DB context configures the following behaviour for Customer:

builder.Entity<Customer>()
    .OwnsOne(o => o.Address);
builder.Entity<Customer>()
    .OwnsOne(o => o.Contact);

Calling AddAsync successfully adds the entity to the DB, calling UpdateAsync will update only the top level non nested properties.

When I try to update the entity with a new Address or Contact, the values aren't updated. But if I update FirstName it works fine. Should this be working as I expect or am I missing something?

Provider and version information

EF Core version: 6.0.7 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0 Operating system: Windows 11 IDE: 2022.1.2

ScottKane commented 2 years ago

Most minimal repro I can show: https://github.com/ScottKane/SetValues

Set Server as startup project Update DB Run

Add a Customer, It will the appear in the table. Click the edit icon and try to change FirstName or LastName, it will work, then try updating House Number, Street or Email which wont work. Observe each of the handlers under Server\Handlers to debug and see behaviour

ScottKane commented 2 years ago

Also I'm aware that using a UnitOfWork/Repository pattern with EF is redundant, but the bigger application this comes from allows switching ORM's as well as the application layer calling the repository has no knowledge of the DbContext and that is why it's being used

ajcvickers commented 2 years ago

@ScottKane There's a lot of code here, but as far as I can tell there is nothing that actually updates the dependent entities. I think it might be worth reading through Explicitly Tracking Entities to understand how EF Core change tracking works with graphs of entities.

ScottKane commented 2 years ago

Get this entity with the given Id, set its values to that of this given object e.g:

DB has this representation of Person

public class Person
{
    public int Id{ get; set; } = 1
    public string Name { get; set; } = "Bob"
}

var selected = await _dbContext.Set<Person>().FindAsync(entity.Id);
if (selected is not null) // Got Bob
{
    _dbContext.Entry(selected).CurrentValues.SetValues(
        new Person
        {
            Id = 1,
            Name = "John"
        });
    await _dbContext.SaveChangesAsync();
}

The value for Person with an Id of 1 should now return "John" and not "Bob", which it does.

However if we take the following example:

public class Person
{
    public int Id{ get; set; } = 1
    public string Name { get; set; } = "Bob"
    public Address Address { get; set; } = new Address { Street = "Drury Lane" };
}

public class Address
{
    public string Street { get; set; }
}

var selected = await _dbContext.Set<Person>().FindAsync(entity.Id);
if (selected is not null) // Got Bob
{
     _dbContext.Entry(selected).CurrentValues.SetValues(
        new Person
        {
            Id = selected.Id,
            Name = selected.Name,
            Address = new Address { Street = "Abbey Road" }
        });
    await _dbContext.SaveChangesAsync();
}

The entity would still have and Address.Street of "Drury Lane" and not "Abbey Road" as expected.

So I can just use context.Update() instead? That's fine but I'm still fairly sure this is a bug.

ScottKane commented 2 years ago

Update doesn't work, it thinks I'm trying to add a new entity:

System.InvalidOperationException: The instance of entity type 'Customer' cannot be tracked because another instance with the same key value for {'Id'} is already b
eing tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSe
nsitiveDataLogging' to see the conflicting key values.

_dbContext.Entry(selected).CurrentValues.SetValues() works fine until I need to update a nested object.

ScottKane commented 2 years ago

I think this may be because the mapper bringing in the DTO and mapping to a Customer is missing some of the DB fields, forcing the mapped DTO (now a Customer) to re-map that customer to the selected customer seems to work.

ajcvickers commented 2 years ago

@ScottKane SetValues works on a single entity instance. It does not set values for related entity instances.

ScottKane commented 2 years ago

@ajcvickers well I'm using it to do that and it definitely is updating the related entity. I'm assuming this is because a Customer owns an Address so it's effectively treaded as part of the entity? I'm never using this to update a one to many relationship as there is a separate API for the child entity to be updated.

n0099 commented 3 months ago

https://github.com/dotnet/efcore/issues/14626#issuecomment-461883036 https://stackoverflow.com/questions/11705569/using-the-entrytentity-currentvalues-setvalues-is-not-updating-collections Workaround to manually fill navigation props: https://stackoverflow.com/questions/66206459/update-navigation-property-with-entity-currentvalues-setvalues/66491805#66491805