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

Owned types in table per hierarchy cause duplicate rowversion column in query that then throws system exception #27888

Closed
KillerBoogie opened this issue Apr 26, 2022 · 3 comments · Fixed by #28958
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@KillerBoogie
Copy link

KillerBoogie commented Apr 26, 2022

We ran into a major issue using table per hierarchy. The base class has a RowVersion attribute. When the table is queried the created query includes the RowVersion column twice and crashes with

System.InvalidCastException: 'Unable to cast object of type 'System.Byte[]' to type 'System.Decimal'.'

This only happens when a derived class has an owned type and therefore no converter!

Here is an example:

LINQ

Guid accountId = new Guid("da19e7f2-2520-48fd-9a52-d90c5e2d1e6b");
Account? account = _contest.Account.SingleOrDefault(e => e.Id == accountId);

Created Query: [a].[_etag] is doubled in select

DECLARE @__accountId_0 uniqueIdentifier = 'da19e7f2-2520-48fd-9a52-d90c5e2d1e6b';

SELECT [a].[Id], [a].[Type], [a].[_etag], [a].[FirstName], [a].[LastName], [a].[_etag]
FROM [AC].[Account] AS [a]
WHERE [a].[Id] = @__accountId_0

Classes

public abstract class Account
{
    public Guid Id { get; set; }
    public ulong _etag { get; set; }

    public Account(Guid id, ulong etag)
    {
        Id = id;
        _etag = etag;
    }

    protected Account() { }
}

public class Person : Account
{
    public FullName FullName { get; set; }

    public Person(Guid id, FullName fullName, ulong etag) : base(id, etag)
    {
        FullName = fullName;
    }

    protected Person() { }
}

public class FullName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public FullName(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

Configuration

public class AccountConfiguration : IEntityTypeConfiguration<Account>
{
   public void Configure(EntityTypeBuilder<Account> entity)
   {
       entity.ToTable("Account", "AC");

       entity.HasDiscriminator<string>("Type");

       entity.Property(e => e._etag).IsRowVersion().HasConversion<byte[]>();
   }
}

public class PersonConfiguration : IEntityTypeConfiguration<Person>
{
   public void Configure(EntityTypeBuilder<Person> entity)
   {
       entity.ToTable("Account", "AC");

       entity.OwnsOne(e => e.FullName, e =>
       {
           e.Property(e => e.FirstName).HasColumnName("FirstName");
           e.Property(e => e.LastName).HasColumnName("LastName");
       });
   }
}

Include provider and version information

EF Core version: 6.04
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 6.0
Operating system: Win 10
IDE: Visual Studio 2022 17.1.2

@KillerBoogie KillerBoogie changed the title Owned types in table per hierarchy duplicate rowversion column in query which then causes a crash Owned types in table per hierarchy cause duplicate rowversion column in query that then leads to system exception Apr 26, 2022
@KillerBoogie KillerBoogie changed the title Owned types in table per hierarchy cause duplicate rowversion column in query that then leads to system exception Owned types in table per hierarchy cause duplicate rowversion column in query that then throws system exception Apr 26, 2022
@ajcvickers
Copy link
Member

Notes for triage:

Query expression:

dbug: 4/28/2022 15:06:24.380 CoreEventId.QueryExecutionPlanned[10107] (Microsoft.EntityFrameworkCore.Query)
      Generated query execution expression:
      'queryContext => new SingleQueryingEnumerable<Account>(
          (RelationalQueryContext)queryContext,
          RelationalCommandCache.SelectExpression(
              Client Projections:
                  0 -> Dictionary<IProperty, int> { [Property: Account.Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd, 0], [Property: Account.Type (no field, string) Shadow Required AfterSave:Throw, 1], [Property: Account._etag (ulong) Required Concurrency BeforeSave:Ignore AfterSave:Ignore ValueGen
erated.OnAddOrUpdate, 2] }
                  1 -> Dictionary<IProperty, int> { [Property: FullName.PersonId (no field, Guid) Shadow Required PK FK AfterSave:Throw, 0], [Property: FullName.FirstName (string) Required, 3], [Property: FullName.LastName (string) Required, 4], [Property: FullName._TableSharingConcurrencyTokenConvention__etag
(no field, ulong) Shadow Required Concurrency BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate, 5] }
              SELECT TOP(2) a.Id, a.Type, a._etag, a.FirstName, a.LastName, a._etag
              FROM AC.Account AS a
              WHERE a.Id == @__accountId_0),
          Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, Account>,
          SomeDbContext,
          False,
          False,
          True
      )
          .SingleOrDefault()'

Model:

Model: 
  EntityType: Account Abstract
    Properties: 
      Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Type (no field, string) Shadow Required AfterSave:Throw
      _etag (ulong) Required Concurrency BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate
    Keys: 
      Id PK
  EntityType: FullName Owned
    Properties: 
      PersonId (no field, Guid) Shadow Required PK FK AfterSave:Throw
      FirstName (string) Required
      LastName (string) Required
      _TableSharingConcurrencyTokenConvention__etag (no field, ulong) Shadow Required Concurrency BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate
    Keys: 
      PersonId PK
    Foreign keys: 
      FullName {'PersonId'} -> Person {'Id'} Unique Ownership ToDependent: FullName Cascade
  EntityType: Person Base: Account
    Navigations: 
      FullName (FullName) ToDependent FullName

Code:

#nullable enable

public static class Your
{
    public static string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOh";
}

public abstract class Account
{
    public Guid Id { get; set; }
    public ulong _etag { get; set; }

    public Account(Guid id, ulong etag)
    {
        Id = id;
        _etag = etag;
    }

    protected Account() { }
}

public class Person : Account
{
    public FullName FullName { get; set; }

    public Person(Guid id, FullName fullName, ulong etag) : base(id, etag)
    {
        FullName = fullName;
    }

    protected Person() { }
}

public class FullName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public FullName(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

public class AccountConfiguration : IEntityTypeConfiguration<Account>
{
    public void Configure(EntityTypeBuilder<Account> entity)
    {
        entity.ToTable("Account", "AC");
        entity.HasDiscriminator<string>("Type");
        entity.Property(e => e._etag).IsRowVersion().HasConversion<byte[]>();
    }
}

public class PersonConfiguration : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> entity)
    {
        entity.ToTable("Account", "AC");
        entity.OwnsOne(e => e.FullName, e =>
        {
            e.Property(e => e.FirstName).HasColumnName("FirstName");
            e.Property(e => e.LastName).HasColumnName("LastName");
        });
    }
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(Your.ConnectionString)
            .LogTo(Console.WriteLine, LogLevel.Debug)
            .EnableSensitiveDataLogging();

    public DbSet<Account> Accounts { get; set; }
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        new PersonConfiguration().Configure(modelBuilder.Entity<Person>());
        new AccountConfiguration().Configure(modelBuilder.Entity<Account>());
    }
}

public class Program
{
    public static void Main()
    {
        Guid accountId = new Guid("da19e7f2-2520-48fd-9a52-d90c5e2d1e6b");

        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.Add(new Person(accountId, new FullName("A", "V"), 0l));
            context.SaveChanges();
        }
        
        using (var context = new SomeDbContext())
        {
            var account = context.Accounts.SingleOrDefault(e => e.Id == accountId);
        }
    }
}

@AndriySvyryd
Copy link
Member

Could be related to #22584

@KillerBoogie
Copy link
Author

My colleague found a workaround. You need to define the etag property in every OwnsType hat is in the entity. So it seems to be related to #22584

public void Configure(EntityTypeBuilder<Person> entity)
{
    entity.ToTable("Account", "AC");

    entity.OwnsOne(e => e.FullName, e =>
    {
        e.Property(e => e.FirstName).HasColumnName("FirstName");
        e.Property(e => e.LastName).HasColumnName("LastName");
        e.Property("_etag").IsRowVersion().HasConversion<byte[]>();
    });

    entity.OwnsOne(e => e.XYZ, e =>
    { 
       ...
       e.Property("_etag").IsRowVersion().HasConversion<byte[]>();
    });
}

@ajcvickers ajcvickers added this to the 7.0.0 milestone May 2, 2022
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 1, 2022
@AndriySvyryd AndriySvyryd removed their assignment Sep 1, 2022
AndriySvyryd added a commit that referenced this issue Sep 1, 2022
AndriySvyryd added a commit that referenced this issue Sep 1, 2022
AndriySvyryd added a commit that referenced this issue Sep 1, 2022
AndriySvyryd added a commit that referenced this issue Sep 2, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0, 7.0.0-rc2 Sep 9, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0-rc2, 7.0.0 Nov 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
3 participants