-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
File a bug
After updating to EF Core 8 preview 6, our existing code that uses ReferenceEntry.LoadAsync()
no longer works.
The repro code below is an extremely simplified version; our framework JsonApiDotNetCore allows developers to plug in their own DbContext
, so in reality, our code is pretty dynamic. We also aim to execute a minimal amount of queries to the database, for performance. For more background on our framework, see the intro at #27436 (comment).
Include your code
The code below succeeds when adding a NuGet reference to Microsoft.EntityFrameworkCore.Sqlite
v7.0.9. Replacing the version with 8.0.0-preview.6.23329.4 makes the LoadAsync()
method call throw.
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
// Arrange
var order1 = new Order
{
Shipment = new Shipment()
};
var order2 = new Order();
await using (var appDbContext = new AppDbContext(new DbContextOptions<AppDbContext>()))
{
await appDbContext.Database.EnsureDeletedAsync();
await appDbContext.Database.EnsureCreatedAsync();
appDbContext.Orders.AddRange(order1, order2);
await appDbContext.SaveChangesAsync();
}
// Act
await using (var appDbContext = new AppDbContext(new DbContextOptions<AppDbContext>()))
{
// Handle logic for JSON:API request: PATCH /orders/2/relationships/shipment
// {
// type: "shipments"
// id: 1
// }
// Which means: Assign the Shipment (currently attached to order1) to order2, effectively removing it from order1.
long orderToUpdateId = order2.Id;
long shipmentToAssignId = order1.Shipment.Id;
Order orderToUpdate = await appDbContext.Orders
.Include(order => order.Shipment)
.Where(order => order.Id == orderToUpdateId)
.SingleAsync();
Shipment trackedShipmentToAssign = (Shipment)MakeTracked(new Shipment() { Id = shipmentToAssignId }, appDbContext);
// Because we have a 1-to-1 relationship, we must ensure that order1.Shipment gets set to NULL
// to avoid a FK unique constraint violation.
ReferenceEntry inverseToLoad = appDbContext.Entry(trackedShipmentToAssign).Reference("Order");
await inverseToLoad.LoadAsync(); // throws on EF 8
orderToUpdate.Shipment = trackedShipmentToAssign;
await appDbContext.SaveChangesAsync();
}
// Assert
await using (var appDbContext = new AppDbContext(new DbContextOptions<AppDbContext>()))
{
var storedOrder1 = await appDbContext.Orders
.Include(order => order.Shipment)
.Where(order => order.Id == order1.Id)
.SingleAsync();
var storedOrder2 = await appDbContext.Orders
.Include(order => order.Shipment)
.Where(order => order.Id == order2.Id)
.SingleAsync();
Debug.Assert(storedOrder1.Shipment == null);
Debug.Assert(storedOrder2.Shipment.Id == order1.Shipment.Id);
}
static object MakeTracked(object entity, DbContext dbContext)
{
dbContext.Entry(entity).State = EntityState.Unchanged;
return entity;
}
public sealed class Shipment
{
public long Id { get; set; }
public DateTimeOffset ShippedAt { get; set; }
public Order Order { get; set; } = null!;
}
public sealed class Order
{
public long Id { get; set; }
public decimal Amount { get; set; }
public Shipment Shipment { get; set; } = null!;
}
public sealed class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Shipment> Shipments => Set<Shipment>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseSqlite("Data Source=SampleDb.db;Pooling=False");
}
protected override void OnModelCreating(ModelBuilder builder)
{
// By default, Entity Framework Core generates an identifying foreign key for a required 1-to-1 relationship.
// This means no foreign key column is generated, instead the primary keys point to each other directly.
// That mechanism does not make sense for JSON:API, because patching a relationship would result in also
// changing the identity of a resource. Naming the foreign key explicitly fixes the problem by forcing to
// create a foreign key column.
builder.Entity<Order>()
.HasOne(order => order.Shipment)
.WithOne(shipment => shipment.Order)
.HasForeignKey<Shipment>("OrderId");
}
}
Include stack traces
System.InvalidOperationException
HResult=0x80131509
Message=The navigation 'Shipment.Order' cannot be loaded because one or more of the key or foreign key properties are shadow properties and the entity is not being tracked. Relationships using shadow values can only be loaded for tracked entities.
Source=Microsoft.EntityFrameworkCore
StackTrace:
at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.GetLoadValues(INavigation navigation, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.<LoadAsync>d__27.MoveNext()
at Program.<<Main>$>d__0.MoveNext() in D:\Bart\Source\Projects\LoadAsyncBugRepro\LoadAsyncBugRepro\Program.cs:line 44
at Program.<<Main>$>d__0.MoveNext() in D:\Bart\Source\Projects\LoadAsyncBugRepro\LoadAsyncBugRepro\Program.cs:line 48
Include provider and version information
EF Core version: 8.0.0-preview.6.23329.4
Database provider: Sqlite (originally PostgreSQL, but there's no compatible preview-6 release yet)
Target framework: .NET 8 preview6
Operating system: Windows 11
IDE: Visual Studio 2022 Community v17.7.0 Preview 3