SQL Server EF Core Transaction Order and Foreign Key Constraint

bnlyeluc  于 2023-10-15  发布在  其他
关注(0)|答案(2)|浏览(122)

Issue with EF Core transaction order and foreign key constraint

Description

I'm facing an issue with Entity Framework Core while trying to add both a User and an Order entity in the same transaction. In my application, a User can have multiple Order s, and each Order must have exactly one User .

I expect both entities to be created in the correct order - first the User , then the Order , so the foreign key relationship can be resolved successfully. However, EF Core seems to attempt to create the Order before the User , leading to a foreign key constraint violation.

Important Information

  • This issue is part of a simplified version of a domain-driven-design project. It's worth noting that my Order entity does not have a navigation property to User as they belong to different "domains." I believe that the foreign key int UserId should be sufficient for this relationship.
  • I have tried calling SaveChanges() after adding the User to the DbContext but BEFORE adding the Order , and it works. However, this approach results in two separate transactions, which is not acceptable for my use-case.

Code

I have provided a runnable repository with all the relevant code in Worker.cs , MyDbContext.cs , User.cs , and Order.cs . The core of the code looks like this:

using (var scope = _serviceScopeFactory.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    var user = new User("Test user");
    var order = new Order(user);

    dbContext.Users.Add(user);
    dbContext.Orders.Add(order);

    await dbContext.SaveChangesAsync();
}

Stack Traces

Here are the relevant parts of the stack trace:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
  Executed DbCommand (40ms) [Parameters=[@p0='?' (DbType = Int32), @p1='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
  SET NOCOUNT ON;
  INSERT INTO [Order] ([UserId])
  OUTPUT INSERTED.[Id]
  VALUES (@p0);
  INSERT INTO [User] ([Name])
  OUTPUT INSERTED.[Id]
  VALUES (@p1);
fail: Microsoft.EntityFrameworkCore.Update[10000]
  An exception occurred in the database while saving changes for context type 'Efentityorderworker.MyDbContext'.
  Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
   ---> Microsoft.Data.SqlClient.SqlException (0x80131904): The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Order_User_UserId". The conflict occurred in database "db-eforder-test", table "dbo.User", column 'Id'.

I'm looking for advice on how to ensure that EF Core creates the User entity before the Order entity within the same transaction, considering my domain design and constraints.

Any help or suggestions would be greatly appreciated!

goqiplq2

goqiplq21#

Order entity does not have a navigation property to User . . . I believe that the foreign key int UserId should be sufficient for this relationship.

Nope. EF doesn't understand Foreign Key properties without Navigation Properties. Without a Navigation Property EF will not know how to order the inserts, and you would have to force that by calling SaveChanges multiple times.

5us2dqdw

5us2dqdw2#

Order entity does not have a navigation property to User . . . I believe that the foreign key int UserId should be sufficient for this relationship.

That's correct, EF Core supports that. And correctly reorders insert/update/delete operations. As soon as you provide the correct data.

Let take a look at your example:

var user = new User("Test user");
var order = new Order(user);

dbContext.Users.Add(user);
dbContext.Orders.Add(order);

Since you are using only Ids, ask yourself, what is the user.Id here

var order = new Order(user);

The answer is 0. And this is what EF is trying to insert and is failing with FK constraint violation.

It can easily be seen if you insert at the end

var info = dbContext.ChangeTracker.ToDebugString();

which will show

Order {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  UserId: 0 FK
User {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  Name: 'Test user'

This is a fundamental issue with new entities with auto generated keys - the actual Id is not available until you call SaveChanges{Async} . EF Core actually associates a temporary value to it when you call Add , but starting from some version does not apply it to the entity, so the entity property still has value 0, i.e. even you do this

var user = new User("Test user");
dbContext.Users.Add(user);

// user.Id is still 0

Now, this is one of the problems of not having at least one navigation property. If you have so, you would simply set it (or add to the inverse entity collection) and EF navigation property fixup will take care of PKs/FKs and correct order.

Since you don't have any navigation, you have to use ugly workarounds. For instance, to let EF assign temporary Id value to the entity and still keep it marked as temporary, you could use the following:

var user = new User("Test user");
var userId = dbContext.Add(user).Property(e => e.Id);
// user.Id is 0, userId.CurrentValue is some temp value
userId.IsTemporary = userId.IsTemporary; // <--
// user.Id is now userId.CurrentValue 
 
var order = new Order(user);
dbContext.Add(order);

var info = dbContext.ChangeTracker.ToDebugString();

Ugly right? And can break in any future EF version. The only good thing is that now the info shows

Order {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  UserId: -2147482647 FK
User {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  Name: 'Test user'

and SaveChanges runs successfully with the log something like this

Executed DbCommand (38ms) [Parameters=[@p0='Test user' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
  SET IMPLICIT_TRANSACTIONS OFF;
  SET NOCOUNT ON;
  INSERT INTO [dbo].[User] ([Name])
  OUTPUT INSERTED.[Id]
  VALUES (@p0);

  Executed DbCommand (5ms) [Parameters=[@p1='3'], CommandType='Text', CommandTimeout='30']
  SET IMPLICIT_TRANSACTIONS OFF;
  SET NOCOUNT ON;
  INSERT INTO [dbo].[Order] ([UserId])
  OUTPUT INSERTED.[Id]
  VALUES (@p1);

相关问题