Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use ExecuteUpdate or ExecuteDelete with properties of owned entities #30833

Closed
wsimf opened this issue May 6, 2023 · 4 comments
Closed

Comments

@wsimf
Copy link

wsimf commented May 6, 2023

EF Core throws a System.InvalidOperation when trying to bulk update (with RelationalQueryableExtensions.ExecuteUpdate<TSource> (link) (or delete) property of an owned entity through the owner.

Schema

Owned Entity:

public class EntityChangeTime
{
    public DateTime Created { get; set; }
    public DateTime? Modified { get; set; }
    public DateTime? Deleted { get; set; }
}

Owners:

public class File
{
    public long FileId { get; set; 
    public EntityChangeTime Date { get; set; } = new(); // owned entity
    public ulong RowVersion { get; set; }

    public ICollection<FileVersion> Versions = new HashSet<FileVersion>();
}

public class FileVersion
{
    public long FileId { get; set; }
    public int Version { get; set; }
    public EntityChangeTime Date { get; set; } = new(); // owned entity
    public ulong RowVersion { get; set; }

    public File File { get; set; } = null!;
}

Entity Configuration:

public class FileConfiguration : IEntityTypeConfiguration<File>
{
    public void Configure(EntityTypeBuilder<File> builder)
    {
        builder.HasKey(e => e.FileId);
        builder.Property(e => e.FileId).ValueGeneratedOnAdd();
        builder.Property(e => e.RowVersion)
            .IsRowVersion()
            .HasConversion<byte[]>();

        builder.HasMany(e => e.Versions)
            .WithOne(e => e.File)
            .HasForeignKey(e => e.FileId)
            .IsRequired();

        builder.OwnsOne(f => f.Date); // owned entity config
    }
}

public class FileVersionConfiguration : IEntityTypeConfiguration<FileVersion>
{
    public void Configure(EntityTypeBuilder<FileVersion> builder)
    {
        builder.HasKey(e => new { e.Version, e.FileId });
        builder.Property(e => e.RowVersion)
            .IsRowVersion()
            .HasConversion<byte[]>();

        builder.OwnsOne(v => v.Date); // owned entity config
    }
}

Generated Migration:

public partial class Initial : Migration
{
    /// <inheritdoc />
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.EnsureSchema(
            name: "DOC_MANAGER");

        migrationBuilder.CreateTable(
            name: "Files",
            schema: "DOC_MANAGER",
            columns: table => new
            {
                FileId = table.Column<long>(type: "bigint", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Date_Created = table.Column<DateTime>(type: "datetime2", nullable: false),
                Date_Modified = table.Column<DateTime>(type: "datetime2", nullable: true),
                Date_Deleted = table.Column<DateTime>(type: "datetime2", nullable: true),
                RowVersion = table.Column<byte[]>(type: "rowversion", rowVersion: true, nullable: false)
            },
            constraints: table => { table.PrimaryKey("PK_Files", x => x.FileId); });

        migrationBuilder.CreateTable(
            name: "FileVersions",
            schema: "DOC_MANAGER",
            columns: table => new
            {
                FileId = table.Column<long>(type: "bigint", nullable: false),
                Version = table.Column<int>(type: "int", nullable: false),
                Date_Created = table.Column<DateTime>(type: "datetime2", nullable: false),
                Date_Modified = table.Column<DateTime>(type: "datetime2", nullable: true),
                Date_Deleted = table.Column<DateTime>(type: "datetime2", nullable: true),
                RowVersion = table.Column<byte[]>(type: "rowversion", rowVersion: true, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_FileVersions", x => new { x.Version, x.FileId });
                table.ForeignKey(
                    name: "FK_FileVersions_Files_FileId",
                    column: x => x.FileId,
                    principalSchema: "DOC_MANAGER",
                    principalTable: "Files",
                    principalColumn: "FileId",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateIndex(
            name: "IX_FileVersions_FileId",
            schema: "DOC_MANAGER",
            table: "FileVersions",
            column: "FileId");
    }

    /// <inheritdoc />
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "FileVersions",
            schema: "DOC_MANAGER");

        migrationBuilder.DropTable(
            name: "Files",
            schema: "DOC_MANAGER");
    }
}

Repository class where ExecuteUpdate is used:

public class FileRepository
{
    private readonly IFileEntityProvider _context;

    public FileRepository(IFileEntityProvider context)
    {
        _context = context;
    }

    public async Task DeleteFile(long fileId)
    {
        File? file = await _context.Files.FindAsync(fileId).ConfigureAwait(false);
        FileDataNotFoundException.ThrowIfNull(file, fileId);

        DateTime deleted = DateTime.UtcNow;
        file.Date.Deleted = deleted;

        await using IDbContextTransaction transaction =
            await _context.Database.BeginTransactionAsync(IsolationLevel.Snapshot);

        try
        {
            _context.Files.Update(file);
            await _context.SaveChangesAsync();

            await _context.FileVersions
                .Where(v => v.FileId == fileId)
                .ExecuteUpdateAsync(conf =>
                    conf.SetProperty(v => v.Date.Deleted, deleted)); // <- Exception here

            await transaction.CommitAsync().ConfigureAwait(false);
        }
        catch (Exception)
        {
            await transaction.RollbackAsync().ConfigureAwait(false);
            throw;
        }
    }
}

Stack trace

System.InvalidOperationException : The LINQ expression 'DbSet<FileVersion>()
    .Where(f => f.FileId == __request_FileId_0)
    .Select(f => IncludeExpression(
        EntityExpression:
        IncludeExpression(
            EntityExpression:
            f, 
            NavigationExpression:
            EF.Property<EntityChangeTime>(f, "Date"), Date)
        , 
        NavigationExpression:
        EF.Property<EntityChangeUser>(f, "User"), User)
    )
    .ExecuteUpdate(conf => conf.SetProperty<DateTime?>(
        propertyExpression: v => v.Date.Deleted, 
        valueExpression: __request_DeletedTime_1))' could not be translated. Additional information: The following lambda argument to 'SetProperty' does not represent a valid property to be set: 'v => v.Date.Deleted'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.ExecuteUpdateAsync[TSource](IQueryable`1 source, Expression`1 setPropertyCalls, CancellationToken cancellationToken)
   at DocumentManager.Data.Core.Repository.Default.FileRepository.<>c__DisplayClass7_0.<<DeleteFile>b__0>d.MoveNext() in /Users/sudara/Developer/DocumentManager/Data/DocumentManager.Data.Core/Repository/Default/FileRepository.cs:line 110

Pprovider and version information

EF Core version: 7.0.5
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 7.0
Operating system: macOS 13.3.1 (a)
IDE: Jet Brains Rider 2023.1.1

@roji
Copy link
Member

roji commented May 8, 2023

This is a likely duplicate of #30528.

Can you please try to the latest daily build and confirm that the bug is gone there?

@wsimf
Copy link
Author

wsimf commented May 9, 2023

Yes tried a daily build and it has been fixed.

@wsimf wsimf closed this as completed May 9, 2023
@roji
Copy link
Member

roji commented May 9, 2023

Thanks for confirming!

@roji roji closed this as not planned Won't fix, can't repro, duplicate, stale May 9, 2023
@roji
Copy link
Member

roji commented May 9, 2023

Duplicate of #30528.

@roji roji marked this as a duplicate of #30528 May 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants