Update is not working in OwnsMany Value Object relationship EF Core 5

Viewed 580

I have the following classes (reduced for simplicity).

public class User : Entity
{
    public List<Skill> Skills { get; set; }
}

public class Skill : ValueObject
{
    public string Name { get; private set; }
}

My UserConfiguration class is the following:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder
            .OwnsMany(user => user.Skills);
    }
}

Here I am saying EF that my User class owns some Skills and it generates the following table: Skill Table

I use the Unit of Work pattern, so in my repository I do the following on my Update method:

public virtual void Update(T entityToUpdate)
{
    _context.Entry(entityToUpdate).State = EntityState.Modified;
}

And after updating I call my Commit method, which saves the changes:

public void Commit()
{
    _dbContext.SaveChanges();
}

As far as I have been able to see, on getting the User entity I am receiving all the properties correctly (including the Skills), but on updating is updating each property but the Skills.

I have tried to manually set the Skill's table Primary Key and Foreign Key changing the UserConfiguration class this way (with the same result, skills are not updated):

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder
            .OwnsMany(user => user.Skills, skill =>
            {
                skill.WithOwner().HasForeignKey("UserId");
                skill.Property<int>("Id");
                skill.HasKey("id");
            });
    }
}

I have also tried to change my Update method to:

public virtual void Update(T entityToUpdate)
    {
        _dbSet.Update(entityToUpdate);
    }

But I am getting the following exception: Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : Database operation expected to affect 1 row(s) but actually affected 0 row(s).

Note that Skill is a ValueObject (it has a shadow Id). I actually believe there should be the problem.

Thanks a lot for your help.

Edit: Since @pejman answer is not working for me I am going to put here my UnitOfWork, Repository and Context (reduced for simplicity), since are the only things I have not done like @pejman, maybe the problem is right there, but I am not able to see it.

Repository:

class BaseRepository<T> : IRepository<T> where T : Entity
{
    private readonly Context _context;
    private readonly DbSet<T> _dbSet;

    public BaseRepository(Context context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public virtual T GetById(Guid id) => _dbSet.AsNoTracking().FirstOrDefault(x => x.Id == id);

    public virtual void Update(T entityToUpdate)
    {
        _context.Entry(entityToUpdate).State = EntityState.Modified;
    }
}

Unit of work:

public class UnitOfWork : IUnitOfWork
{
    private const int SqlObjectAlreadyExistsErrorNumber = 2627;
    private readonly Context _dbContext;
    private BaseRepository<User> _users;

    public UnitOfWork(Context dbContext)
    {
        _dbContext = dbContext;
    }

    public IRepository<User> Users
    {
        get
        {
            return _users ??= new BaseRepository<User>(_dbContext);
        }
    }

    public void Commit()
    {
        _dbContext.SaveChanges();
    }
}

Context:

public class Context : DbContext
{
    public Context(DbContextOptions<DeribeContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(DeribeContext).Assembly);
        base.OnModelCreating(modelBuilder);
    }
}
2 Answers

Updating collection navigation properties of disconnected entities has very limited support. First off, they are not considered part of the entity data. Second, it's not quite clear what should be done with the existing child entities not found in the disconnected entity.

All this is because EF Core has no information about the original database state, as in their primary supported (and the only reliable) flow of Load (in db context change tracker), Modify, Save.

In theory collections of owned entity types should be handled easier than collection of regular entities. Since they are considered part of the owner (at least when loading), they could have been processed as such during the update, i.e. replace by first deleting all exiting and then insert (add) all incoming.

In practice (current EF Core) updating them in disconnect scenario is virtually impossible. Not only they are not handled automatically, but since you can't define DbSet (hence use Set<T>() method) of owned entity type, there is no other way to tell EF Core to delete existing by actually loading them from the database and clearing the collection. This along with the fact that explicit/lazy loading does not work, leaves the only option of load the owner entity from database.

And once you do that, then better apply the modified properties as well (using CurrentValues.SetValues method), rather than using the "forced update" all which is happening when using Update method (or setting State to EntityState.Modified).

Apply it to your sample model would be something like this

var dbUser = _context.Set<User>().FirstOrDefault(e => e.Id == user.Id);
_context.Entry(dbUser).CurrentValues.SetValues(user);
dbUser.Skills = user.Skills;

Since the only custom code is dbUser.Skills = user.Skills;, it could be generalized for any collection of owned entity types by using EF Core provided metadata and change tracking API:


public virtual void Update(T entityToUpdate)
{
    // Load existing entity from database into change tracker
    var entity = _dbSet.AsTracking()
        .FirstOrDefault(x => x.Id == entityToUpdate.Id);
    if (entity is null)
        throw new KeyNotFoundException();
    var entry = _context.Entry(entity);
    // Copy (replace) primitive properties
    entry.CurrentValues.SetValues(entityToUpdate);
    // Copy (replace) owned collections
    var ownedCollections = entry.Collections
        .Where(c => c.Metadata.TargetEntityType.IsOwned());
    foreach (var collection in ownedCollections)
    {
        collection.CurrentValue = (IEnumerable)collection.Metadata
            .GetGetter().GetClrValue(entityToUpdate);
    }
}

Try this, it worked properly:

 public class User : Entity
{
    private List<Skill> _skills;
    public IReadOnlyList<Skill> Skills => _skills.AsReadOnly();// use this way 
                                                                 //for more encapsulation

    public void AssignSkills(List<Skill> skills) 
    {
        this._skills = skills;
    }
}

and Skill class :

public class Skill
{
    public Skill(string name, string desc)
    {
        this.Name = name;
        Description = desc;
    }

    protected Skill() { }
    public string Name { get; private set; }
    public string Description { get; private set; }
}
 
public class Entity
{
    public int Id { get; set; }
}

for Mapping:

 public class UserMapping : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property(a => a.Id).ValueGeneratedNever();
        builder.OwnsMany(a => a.Skills, map =>
        {
            map.ToTable("Skills").HasKey("Id");
            map.Property<int>("Id");
            map.WithOwner().HasForeignKey("UserId");
            map.UsePropertyAccessMode(PropertyAccessMode.Field);
        });
    }
}

and MyContext:

public class MyDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=users;Integrated Security=true");
        base.OnConfiguring(optionsBuilder);
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(UserMapping).Assembly);
        base.OnModelCreating(modelBuilder);
    }
}

and in last which i try to add a list of skills to user then update the list, and it worked properly:

 static async Task Main(string[] args)
    {
        using (var dbContext = new MyDbContext())
        {
            var user = new User();
            var skills = new List<Skill>()
            {
                new Skill("first skill","desc1"),
                new Skill("sec skill","desc2"),
            };
            user.AssignSkills(skills);

            dbContext.Users.Add(user);
            dbContext.SaveChanges();
        }

        using (var readContext = new MyDbContext())
        {
            var user = readContext.Users.First();
            var skills = new List<Skill>()
            {
                new Skill("first skill","desc1"),
                new Skill("sec skill","desc_change"),
                new Skill("third skill","desc_New"),
            };
            user.AssignSkills(skills);
            await readContext.SaveChangesAsync();
        }
Related