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 get attached multiple times as different types #26257

Closed
berhir opened this issue Oct 5, 2021 · 1 comment · Fixed by #28987
Closed

Owned Types get attached multiple times as different types #26257

berhir opened this issue Oct 5, 2021 · 1 comment · Fixed by #28987
Labels
area-cosmos area-query 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

@berhir
Copy link

berhir commented Oct 5, 2021

I try to implement a simple activity stream with EF and Cosmos DB. This is a very simplified version of the structure I want to save:

{
    "Id": "782a8d66-db2f-4391-a664-07cc1e636688",
    "Type": "OrderUpdatedActivity",
    "Actor": {
        "Type": "UserActor",
        "ActorId": 1,
        "DisplayName": "User 1"
    }
},
{
    "Id": "cc3a1fb1-8aa0-4bfe-91b6-ed293d45179e",
    "Type": "OrderAcceptedByDriverActivity",
    "Actor": {
        "Type": "DriverActor"
        "ActorId": 2,
        "DisplayName": "Driver 2",
        "LicenseId": 10
    }
}

There are different types of activities, but all have the same base class. The Type property is used as the discriminator. All activities have an Actor property, but actors can be of different types with different properties. All actors have the same base class.

As far as I understand it's not possible to use a discriminator with Owned Types. That's why I created activity classes with specific actor types.

Adding activities to Cosmos DB works already and the structure looks like described before. But when I try to read the items, I get the following exception:

System.InvalidOperationException
  HResult=0x80131509
  Message=Nullable object must have a value.
  Source=System.Private.CoreLib
  StackTrace:
   at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_NoValue()
   at Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable`1.AsyncEnumerator.<MoveNextAsync>d__16.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__65`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.<ToListAsync>d__65`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Program.<<Main>$>d__0.MoveNext() in C:\Users\bhi\source\repos\CosmosEfRepro\CosmosEfRepro\Program.cs:line 38

  This exception was originally thrown at this call stack:
    [External Code]
    Program.<Main>$(string[]) in Program.cs

It seems that EF tries to attach the actors of both activities as type UserActor AND type DriverActor at the same time. The UserActor has no LicenseId and that's why the exception gets thrown.
This is the log output when I try to read the two items I showed before:

dbug: 05.10.2021 18:02:48.579 CoreEventId.QueryCompilationStarting[10111] (Microsoft.EntityFrameworkCore.Query)
      Compiling query expression:
      'DbSet<Activity>()'
dbug: 05.10.2021 18:02:48.658 CoreEventId.NavigationBaseIncluded[10112] (Microsoft.EntityFrameworkCore.Query)
      Including navigation: 'OrderAcceptedByDriverActivity.Actor'.
dbug: 05.10.2021 18:02:48.660 CoreEventId.NavigationBaseIncluded[10112] (Microsoft.EntityFrameworkCore.Query)
      Including navigation: 'OrderUpdatedActivity.Actor'.
dbug: 05.10.2021 18:02:49.082 CoreEventId.QueryExecutionPlanned[10107] (Microsoft.EntityFrameworkCore.Query)
      Generated query execution expression:
      'queryContext => new QueryingEnumerable<Activity>(
          (CosmosQueryContext)queryContext,
          SqlExpressionFactory,
          QuerySqlGeneratorFactory,
          [Cosmos.Query.Internal.SelectExpression],
          Func<QueryContext, JObject, Activity>,
          ActivityContext,
          null,
          False,
          True
      )'
info: 05.10.2021 18:02:49.285 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'ActivitiesContainer' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE c["Type"] IN ("OrderAcceptedByDriverActivity", "OrderUpdatedActivity")
info: 05.10.2021 18:02:54.982 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (4051.6346 ms, 2.96 RU) ActivityId='7128bf76-4e66-422e-8f02-81a724728520', Container='ActivitiesContainer', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE c["Type"] IN ("OrderAcceptedByDriverActivity", "OrderUpdatedActivity")
dbug: 05.10.2021 18:02:55.087 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'OrderUpdatedActivity' entity with key '{Id: 782a8d66-db2f-4391-a664-07cc1e636688}'.
dbug: 05.10.2021 18:02:55.149 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'UserActor' entity with key '{OrderUpdatedActivityId: 782a8d66-db2f-4391-a664-07cc1e636688}'.
dbug: 05.10.2021 18:02:55.201 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'DriverActor' entity with key '{OrderAcceptedByDriverActivityId: 782a8d66-db2f-4391-a664-07cc1e636688}'.
dbug: 05.10.2021 18:02:55.228 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'OrderAcceptedByDriverActivity' entity with key '{Id: cc3a1fb1-8aa0-4bfe-91b6-ed293d45179e}'.
dbug: 05.10.2021 18:02:55.234 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'UserActor' entity with key '{OrderUpdatedActivityId: cc3a1fb1-8aa0-4bfe-91b6-ed293d45179e}'.
dbug: 05.10.2021 18:02:55.236 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
      Context 'ActivityContext' started tracking 'DriverActor' entity with key '{OrderAcceptedByDriverActivityId: cc3a1fb1-8aa0-4bfe-91b6-ed293d45179e}'.

As you can see, both the UserActor and the DriverActor appear twice. But there is only one item with UserActor and one with DriverActor.

The debug view of the model looks fine to me:

Model: 
  EntityType: Activity Abstract
    Properties: 
      Id (string) Required PK AfterSave:Throw
      Type (string) Required AfterSave:Throw
      __id (no field, string) Shadow Required AlternateKey AfterSave:Throw
        Annotations: 
          Cosmos:PropertyName: id
      __jObject (no field, JObject) Shadow
        Annotations: 
          Cosmos:PropertyName: 
    Keys: 
      Id PK
      __id
    Annotations: 
      Cosmos:ContainerName: ActivitiesContainer
      DiscriminatorProperty: Type
      DiscriminatorValue: Activity
  EntityType: DriverActor Owned
    Properties: 
      OrderAcceptedByDriverActivityId (no field, string) Shadow Required PK FK AfterSave:Throw
      ActorId (int) Required
      DisplayName (string) Required
      LicenseId (int) Required
      Type (no field, string) Required
      __jObject (no field, JObject) Shadow BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate
        Annotations: 
          Cosmos:PropertyName: 
    Keys: 
      OrderAcceptedByDriverActivityId PK
    Foreign keys: 
      DriverActor {'OrderAcceptedByDriverActivityId'} -> OrderAcceptedByDriverActivity {'Id'} Unique Ownership ToDependent: Actor Cascade
    Annotations: 
      DiscriminatorProperty: 
  EntityType: OrderAcceptedByDriverActivity Base: Activity
    Navigations: 
      Actor (DriverActor) ToDependent DriverActor
        Annotations: 
          EagerLoaded: True
    Annotations: 
      DiscriminatorProperty: Type
      DiscriminatorValue: OrderAcceptedByDriverActivity
  EntityType: OrderUpdatedActivity Base: Activity
    Navigations: 
      Actor (UserActor) ToDependent UserActor
        Annotations: 
          EagerLoaded: True
    Annotations: 
      DiscriminatorProperty: Type
      DiscriminatorValue: OrderUpdatedActivity
  EntityType: UserActor Owned
    Properties: 
      OrderUpdatedActivityId (no field, string) Shadow Required PK FK AfterSave:Throw
      ActorId (int) Required
      DisplayName (string) Required
      Type (no field, string) Required
      __jObject (no field, JObject) Shadow BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate
        Annotations: 
          Cosmos:PropertyName: 
    Keys: 
      OrderUpdatedActivityId PK
    Foreign keys: 
      UserActor {'OrderUpdatedActivityId'} -> OrderUpdatedActivity {'Id'} Unique Ownership ToDependent: Actor Cascade
    Annotations: 
      DiscriminatorProperty: 
Annotations: 
  Cosmos:ContainerName: ActivityContext
  ProductVersion: 6.0.0-rc.1.21452.10

This is a full working Program.cs file to reproduce the issue:

// See https://aka.ms/new-console-template for more information
using Microsoft.EntityFrameworkCore;

Console.WriteLine("Started");

using (var context = new ActivityContext())
{
    await context.Database.EnsureCreatedAsync();

    context.Activities.Add(new OrderUpdatedActivity
    {
        Id = Guid.NewGuid().ToString(),
        Actor = new UserActor
        {
            ActorId = 1,
            DisplayName = "User 1",
        },
    });

    context.Activities.Add(new OrderAcceptedByDriverActivity
    {
        Id = Guid.NewGuid().ToString(),
        Actor = new DriverActor
        {
            ActorId = 2,
            DisplayName = "Driver 2",
            LicenseId = 10,
        },
    });

    await context.SaveChangesAsync();
    Console.WriteLine("Items saved");
}

using (var context = new ActivityContext())
{
    // this fails with exception "Nullable object must have a value."
    var activities = await context.Activities.ToListAsync();

    foreach (var activity in activities)
    {
        Console.WriteLine($"{activity.Id} | {activity.Actor?.DisplayName} ({activity.Actor?.GetType()})");
    }
}

Console.WriteLine("Press any key to exit");
Console.ReadKey(true);

abstract class Activity
{
    public string Type { get; set; }

    public string Id { get; set; }

    public ActivityActor Actor { get; set; }
}

abstract class Activity<TActor> : Activity
    where TActor : ActivityActor
{
    public new TActor Actor
    {
        get => (TActor)base.Actor;
        set => base.Actor = value;
    }
}

abstract class ActivityActor
{
    public abstract string Type { get; set; }

    public int ActorId { get; set; }

    public string DisplayName { get; set; }
}

class UserActor : ActivityActor
{
    public override string Type
    {
        get => nameof(UserActor);
        set { /* Ignore */ }
    }
}

class DriverActor : UserActor
{
    public override string Type
    {
        get => nameof(DriverActor);
        set { /* Ignore */ }
    }

    public int LicenseId { get; set; }
}

class OrderUpdatedActivity : Activity<UserActor>
{
}

class OrderAcceptedByDriverActivity : Activity<DriverActor>
{
}

class ActivityContext : DbContext
{
    public DbSet<Activity> Activities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder
            .UseCosmos(
                "<endpoint>",
                "<key>",
                "<db>")
            .LogTo(Console.WriteLine)
            .EnableDetailedErrors()
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Activity>()
            .ToContainer("ActivitiesContainer")
            .Ignore(e => e.Actor)
            .HasDiscriminator(e => e.Type)
            .HasValue<OrderUpdatedActivity>(nameof(OrderUpdatedActivity))
            .HasValue<OrderAcceptedByDriverActivity>(nameof(OrderAcceptedByDriverActivity));

        modelBuilder.Entity<OrderAcceptedByDriverActivity>()
            .OwnsOne(e => e.Actor);

        modelBuilder.Entity<OrderUpdatedActivity>()
            .OwnsOne(e => e.Actor);
    }
}

EF Core version: 6.0.0-rc.1.21452.10
Database provider: (e.g. Microsoft.EntityFrameworkCore.Cosmos)
Target framework: (e.g. .NET 6.0)
Operating system: Windows 11
IDE: (e.g. Visual Studio 2022 17.0.0 Preview 4.1)

@ajcvickers
Copy link
Member

Note for triage: not a regression.

@ajcvickers ajcvickers modified the milestones: Backlog, 7.0.0 Nov 10, 2021
@ajcvickers ajcvickers self-assigned this Sep 2, 2022
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 6, 2022
ajcvickers added a commit that referenced this issue Sep 6, 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
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-cosmos area-query 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