dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.8k stars 3.2k forks source link

ValueGenerator doesn't use NextAsync for the added object by the entity relation after SaveChangesAsync #30936

Closed don-flamingo closed 1 year ago

don-flamingo commented 1 year ago

File a bug

When I have added an entity by the inner relation, without invoking the AddAsync method. After SaveChangesAsync my attached ValueGenerator doesn't use the override NextAsync method to generate async value, instead of it uses sync method.

Include your code

public class Relation
{
    public string Key { get; set; }
    public Guid AggregateId { get; set; }
    public int Number { get; set; }
}

public class Aggregate {
    public Guid Id { get; private set; }
    public ICollection<Relation> Relations { get; private set; }

    // ef required
    private Aggregate()
    {

    }

    public Aggregate(Guid id)
    {
        Id = id;
        Relations = new List<Relation>();
    }

    public void AddRelation(string key)
    {
        Relations.Add(new Relation
        {
            AggregateId = Id,
            Key = key,
        });
    }
}

public class RelationNumberValueGenerator : ValueGenerator<int>
{
    public override int Next(EntityEntry entry)
    {
        // Will be used, but should not be
        return 0;
    }

    public override ValueTask<int> NextAsync(EntityEntry entry, CancellationToken cancellationToken = new CancellationToken())
    {
        // Will not be used, but should do
        return new(1);
    }

    public override bool GeneratesTemporaryValues => false;
}

public class RelationEntityTypeConfiguration : IEntityTypeConfiguration<Relation>
{
    public void Configure(EntityTypeBuilder<Relation> builder)
    {
        builder.HasKey(x => x.Key);

        builder.Property(x => x.Number)
            .HasValueGenerator<RelationNumberValueGenerator>();

        builder.HasOne<Aggregate>()
            .WithMany(x => x.Relations)
            .HasForeignKey(x => x.AggregateId);
    }
}

public class AggregateEntityTypeConfiguration : IEntityTypeConfiguration<Aggregate>
{
    public void Configure(EntityTypeBuilder<Aggregate> builder)
    {
        builder.HasKey(x => x.Id);
    }
}

public class AggDbContext : DbContext
{
    public DbSet<Aggregate> Aggregates => Set<Aggregate>();
    public DbSet<Relation> Relations => Set<Relation>();

    public AggDbContext(DbContextOptions<AggDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AggDbContext).Assembly);
    }
}

var context = new AggDbContext(
    new DbContextOptionsBuilder<AggDbContext>()
    .UseNpgsql("Host=localhost;Database=agg;Username=postgres;Password=postgres")
    .Options);

var agg = context.Aggregates
    .AsTracking()
    .First();

agg.AddRelation("test");

await context.SaveChangesAsync();

Include stack traces

NONE

Include verbose output

it's caused by:

        public virtual async Task<int> SaveChangesAsync(
            bool acceptAllChangesOnSuccess,
            CancellationToken cancellationToken = default)
        {
            CheckDisposed();

            SavingChanges?.Invoke(this, new SavingChangesEventArgs(acceptAllChangesOnSuccess));

            var interceptionResult = await DbContextDependencies.UpdateLogger
                .SaveChangesStartingAsync(this, cancellationToken).ConfigureAwait(acceptAllChangesOnSuccess);

            // This method should be async
            TryDetectChanges();
}

Include provider and version information

EF Core version: Database provider: Npgsql.EntityFrameworkCore.PostgreSQL Target framework: .NET 6.0 / EF 6.0.6 Operating system: Mac os

ajcvickers commented 1 year ago

This is currently by-design. If async value generation is needed, then AddAsync must be used to track the entity. This is because, in the general case, an entity can be tracked in many non-async contexts, most notably when DetectChanges is called implicitly by a non-async method.

Note for triage: with HiLo becoming more useful since EF Core 7, async value gen is likely to get more use. While the general case can't be changed, we could re-introduce DetectChangesAsync, which could in turn be called from SaveChangesAsync.

roji commented 1 year ago

One additional radical/spicy thought: we could also move value generation to SaveChanges-time, rather than doing it at Add-time. The main advantage here is that we'd be able to obsolete AddAsync, which seems to cause quite a bit of confusion (and is indeed rarely needed). This would also align client-side value generation to happen at the same time as database-generated, which would make our behavior a bit more consistent.

Of course, this would mean users need to wait until SaveChanges before they can copy out the generated property (again, just like database-generated). This is related to #30753, where we may want to switch to server-generated GUIDs by default (it represents the same minor breaking change).

ajcvickers commented 1 year ago

@roji One of the original motivations for Hi/Lo was that it generated real IDs as soon as the entity is tracked--same reason for client-side GUIDs. EF6 could never do this, and it was a frequent complaint. We could create a HiLo generator which has the characteristics that you like while not generating IDs until SaveChanges. (i.e. one extra round trip ever x number of calls to SaveChanges.) That would be much simpler in lots of places, but wouldn't match one of the main reasons for doing it in the first place.

roji commented 1 year ago

You're saying that many people complained about not being able to get the real ID before calling SaveChanges? That strikes me as a bit odd, but in that case yeah, the current behavior makes more sense.

roji commented 1 year ago

Duplicate of #30957