Search code examples
c#entity-framework-coredomain-driven-designef-core-6.0

EF Core 6 automatically add navigation property


Imagine I had the following code.

var myEntity = Context.MyEntities.GetById(id);
myEntity.SomeNavigationProperty = new MyNavigationProperty(...);
await Context.SaveChangesAsync();

I have the following mapping for MyEntity

builder.HasOne(o => o.SomeNavigationProperty).WithOne().HasForeignKey<MyEntity>(o => o.SomeNavigationPropertyId);

SaveChangesAsync fail with:

The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.

If I look at the change tracker after save, MyNavigationProperty has been marked as modified and not added.

Versions that work

var myEntity = new MyEntity(...);
myEntity.SomeNavigationProperty = new MyNavigationProperty(...);
await Context.AddAsync(myEntity);
await Context.SaveChangesAsync();

and

var myEntity = Context.MyEntities.GetById(id);
myEntity.SomeNavigationProperty = new MyNavigationProperty(...);
await Context.AddAsync(myEntity.SomeNavigationProperty);
await Context.SaveChangesAsync();

This behavior makes sense to a degree if I read up on https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.addasync?view=efcore-6.0. I do not completely understand why ef core decides to mark the untracked entity as modified instead of added though.

Either way... my problem is that my domain and repository are separated. In other words when the property gets set I do not have access to the context.

Is there a way I can tell the EF Core context to re-add all if its not already added without it complaining about duplicates?

Something like

await Context.RefreshAddAsync(myEntity);

where RefreshAddAsync will skip MyEntity (if it's already added) and mark MyNavigationProperty as added.

Any other suggestions appreciated. Worse case scenario I'll write a version of RefreshAddAsync using reflection, but would like a cleaner solution if there is one.


Solution

  • I ended up doing this which works pretty well.

    public static async Task AddOrUpdateEntityAsync<T>(this ArtBankContext context, T entity)
            where T : DomainEntity
    {
        context.ThrowIfNull();
        entity.ThrowIfNull();
    
        var toProcessQueue = new Queue<DomainEntity>();
        var processed = new List<DomainEntity>();
    
        var tracked = RunWithAutoDetectChangesDisabled(() => context.ChangeTracker
            .Entries<DomainEntity>()
            .Select(o => o.Entity));
    
        toProcessQueue.Enqueue(entity);
    
        while (toProcessQueue.Any())
        {
            var toProcess = toProcessQueue.Dequeue();
    
            if (!tracked.Any(o => o == toProcess))
            {
                await context
                    .AddEntityAsync(toProcess)
                        .ContinueOnAnyContext();
            }
    
            foreach (var toProcessPropertyInfo in toProcess
                .GetType()
                .GetProperties()
                .Where(o => o.PropertyType.IsAssignableTo(typeof(DomainEntity))))
            {
                var toProcessProperty = (DomainEntity?)toProcessPropertyInfo.GetValue(toProcess);
    
                if (toProcessProperty == null || processed.Any(o => o == toProcessProperty))
                    continue;
    
                toProcessQueue.Enqueue(toProcessProperty);
            }
    
            processed.Add(toProcess);
        }
    
        TResult RunWithAutoDetectChangesDisabled<TResult>(Func<TResult> func)
        {
            //Ef auto detects changes when you call methods like context.ChangeTracker.Entries
            //In this specific use case the entities that we want to add dynamically gets detected and incorreectly added to the contexts.
            //Hence we disable the flag before executing
            var originalAutoDetectChangesEnabledValue = context.ChangeTracker.AutoDetectChangesEnabled;
    
            context.ChangeTracker.AutoDetectChangesEnabled = false;
    
            var result = func();
    
            context.ChangeTracker.AutoDetectChangesEnabled = originalAutoDetectChangesEnabledValue;
    
            return result;
        }
    }
    

    it is the used like

    var myEntity = Context.MyEntities.GetById(id);
    myEntity.SomeNavigationProperty = new MyNavigationProperty(...);
    await Context.AddOrUpdateEntityAsync(myEntity);
    await Context.SaveChangesAsync();