0

I use the following code to insert a collection of entities into by DB using EntityFrameworkCore.SqlServer (7.0.7). I expect that my returned entities are set with an auto generated id which is a int identity(1,1) not null.

     public async Task<IEnumerable<Instrument>> CreateAsync(IEnumerable<Instrument> instruments)
    {         
        await  DbContext.AddRangeAsync(instruments);
 
        foreach (Instrument entity in instruments)
        {
            var entityEntry = DbContext.Entry(entity);
            Debug.WriteLine($"Entity State: {entityEntry.State}");
        }
        await DbContext.SaveChangesAsync();
        return instruments;
    }

the foreach loop has just been added to check the entity state which is detached . I tried also using DbContext.AddRange(instruments) instead of the async version and the entities are still detached and the id are not set. However if I iterate over my entity and call the following code on each indiviual entity, the is is set with a value:

     public async Task<Instrument> SaveAsync(Instrument entity)
    {
        if (entity.InstrumentId <= 0)
            await DbContext.AddAsync(entity);
        else
            DbContext.UpdateRange(entity);

        await DbContext.SaveChangesAsync();
        return entity;

    }

For what I know, calling AddRangeAsync should attach my entities to the context and set the value of my primary key for each entities. Has someone an idea or faced the same behaviour? Also is my code correct to add new entities into the db? Thanks for the help

Fede
  • 804
  • 1
  • 10
  • 21
  • "AddRangeAsync should attach my entities to the context and set the value of my primary key for each entities." Yes, and No. It should be attaching them, but it will not set the IDs. Identity ID columns will only be set after `SaveChanges` is called. Still that does not explain why they would be reporting as Detached which AFAIK it should be attaching them... – Steve Py Jun 15 '23 at 10:11
  • Thanks Steve, that a good point. However even after the call of SaveChanges the id are not set, while there are in the second case. I case the fact that entities are not attached and the identities not set is related, but this is just a supposition – Fede Jun 15 '23 at 11:26
  • does it produce exception ? EF expression doesn't update primary keys you should use sql native .Why are you updating the ID in the first place ? – thunderkill Jun 15 '23 at 12:42
  • There is no exception thrown. Don't know what you mean by EF expression, but IDs are updated when I use the second code which it is called for each entity from a foreach loop. I have other cases where ids are updated when calling AddRangeAsync. So I expect here the same behavior. The why I need the key is not important here. returning the key of newly created entity could make sense for many reason. In some case it works, why in this case it doesn't? – Fede Jun 15 '23 at 13:21

1 Answers1

0

You will likely need to add some implementation details about how your DbContext is being configured and injected into where this "CreateAsync" call is, and the Instrument entity definition and any configuration. Such as any details like are you disabling change tracking or proxies or using explicit lazy-loading proxies?

I have run a test with AddRange and it worked as advertised. Following is the test code:

    [Test]
    public void TestAddRange()
    {
        using var context = new SoftDeleteDbContext();

        var parents = new Parent[]
        {
            new Parent{ Name = "AddRange1"},
            new Parent{Name ="AddRange2"}
        };
        foreach (var parent in parents)
        {
            var entry = context.Entry(parent);
            Debug.WriteLine($"{parent.ParentId} - {entry.State}");
        }
        context.AddRange(parents);
        foreach (var parent in parents)
        {
            var entry = context.Entry(parent);
            Debug.WriteLine($"{parent.ParentId} - {entry.State}");
        }
        context.SaveChanges();
        foreach (var parent in parents)
        {
            var entry = context.Entry(parent);
            Debug.WriteLine($"{parent.ParentId} - {entry.State}");
        }
    }

Don't mind the entity/DbContext names, this was just from an EF Core UnitTest project I use for testing out and asserting behaviour. The Parent entity uses an Identity column. The output from the first Debug Writelines before the AddRange was:

0 - Detached

0 - Detached

After the AddRange call:

0 - Added

0 - Added

And after the SaveChanges:

6 - Unchanged

7 - Unchanged

So, as mentioned & documented, the AddRange call is marking the items as "Added" while the IDs are set after SaveChanges is called. From the looks of things your AddRange/AddRangeAsync calls aren't actually doing anything against the DbContext which is certainly a curious behaviour.

Some things to check:

  1. Make sure nothing funky is happening with the way the DbContext is referenced in this class. I am assuming that the "DbContext" instance is an injected DbContext. Check that someone hasn't done something silly like:

    private AppDbContext DbContext => new AppDbContext();

... or something similar with a new instance or calling a DbContextFactory to create a new instance. This would have the AddRange and every other call operating against a different DbContext instance.

  1. Check that your application DbContext is not customized and overriding calls like AddRange and AddRangeAsync where they forgot to call base.AddRange(...) leaving the call neutered and ignored.

  2. Also, when testing this, are you running against an SQL Server instance or trying it out against something like an in-memory database? Is this a code-first or DB-first implementation, and have you confirmed that the Instrument table's ID is correctly set up as an Identity? There are certain limitations with in-memory databases so behavior for things like local tests and such can differ compared to runs against an actual database instance.

Hopefully that gives you some ideas to check, otherwise it would help to see the entity definition and any configuration which might shed some light on something possibly tripping it up.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • Helllo steve, thanks a lot for your detailed answer. My DBContext and all the entities are generated using Scaffold-DbContext from an already existing DB. the context is injected into the DAO as follow: var options = services.GetOptions("database"); services.AddDbContext(x => x.UseSqlServer(options.ConnectionString));. My DAO are also injected in the different services as Scopped services. Both codes above are in the same DAO so both rely on the same way the context is created and injected. – Fede Jun 16 '23 at 07:38
  • Another point I have cases where I use the same service with a one to many relation to the Instrument entity. Where many entities A have a foreign key to entity B. In this case if I first save entity B into the db and then save A entites using AddRange, it works. I also checked that there is multithreaded call to the saveasync method, but there is no and I probably would have got an exception in this case – Fede Jun 16 '23 at 07:44