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.63k stars 3.15k forks source link

[3.0 RTM] OwnsMany: Owned/Non-Owned Issue #18168

Closed chris-stormideas closed 1 year ago

chris-stormideas commented 4 years ago

I have raised another issue, however it was incorrectly closed.

The following mapping worked fine in 2.2, no longer works with 3.0. The Cast (of type MovieCastMember) mapping throws. The OwnsMany before works fine, and other OwnsMany throw/work.

The MovieCastMember is not mapped anywhere else. The previous issue was closed due to: https://github.com/aspnet/EntityFrameworkCore/issues/18167#issuecomment-537115340 But it should be clear in the code, this is not the case. There is also a type called CastMember which is not the type that is having the issue.

The MovieCastMember type is a relational object that associates a Movie with a CastMember

public static void Configure(
      this ModelBuilder builder,
      Action<MovieChecklistModelBuilderConfigurationOptions> optionsAction = null)
    {
      Check.NotNull(builder, nameof(builder));

      var options = new MovieModelBuilderConfigurationOptions();

      optionsAction?.Invoke(options);

// These are 3rd party calls and are not aware of my app code.  Kept in for showing full code
      builder.ConfigureIdentity();
      builder.ConfigurePermissionManagement();
      builder.ConfigureSettingManagement();
      builder.ConfigureAuditLogging();
      builder.ConfigureAssets();

      builder.ConfigureRoot<Network>(options, "Networks", b =>
      {
        b.OwnsMany(x => x.FeedCodes)
          .ToTable(options.TablePrefix + "ChannelFeedCodes", options.Schema)
          .HasKey(x => x.Id)
          ;
      });

      builder.ConfigureRoot<Franchise>(options, "Franchises");

      builder.ConfigureRoot<Movie>(options, "Movies", b =>
      {
        b.Ignore(movie => movie.IsPremiere);

        b.OwnsMany(x => x.AirDates, ab =>
        {
          ab.ToTable(options.TablePrefix + "AirDates", options.Schema);

          ab.ConfigureByConvention();
          ab.TryConfigureFullAudited();

          ab.HasKey(x => x.Id);
//            ab.OnDelete(DeleteBehavior.Cascade);
        });

        b.OwnsMany(movie => movie.Cast, cb =>
        {
          cb.ToTable(options.TablePrefix + "MovieCastMembers", options.Schema);

          cb.HasKey(x => new { x.MovieId, x.CastMemberId });

          cb.WithOwner()
            .HasForeignKey(qt => qt.MovieId);

          cb.ConfigureByConvention();
          cb.TryConfigureFullAudited();

          cb.HasOne(x => x.CastMember)
            .WithMany()
            .HasForeignKey(x => x.CastMemberId)
            .IsRequired();

          cb.HasOne(x => x.LandscapeImage)
            .WithMany()
            .HasForeignKey(x => x.LandscapeImageId);

          cb.HasOne(x => x.PortraitImage)
            .WithMany()
            .HasForeignKey(x => x.PortraitImageId);
        });

        b.HasIndex(x => x.UrlName);
        b.HasIndex(x => x.LegacyUrlName);

        b.HasOne(x => x.LandscapeImage)
          .WithMany()
          .HasForeignKey(x => x.LandscapeImageId);

        b.HasOne(x => x.PortraitImage)
          .WithMany()
          .HasForeignKey(x => x.PortraitImageId);

        b.HasOne(x => x.PlaycardImage)
          .WithMany()
          .HasForeignKey(x => x.PlaycardImageId);

        b.HasOne(x => x.PromoVideo)
          .WithMany()
          .HasForeignKey(x => x.PromoVideoId);
      });

      builder.Entity<FranchiseMovie>(options, b => { });

      builder.ConfigureRoot<CastMember>(options, "CastMembers");

      builder.ConfigureRoot<Category>(options, "Categories");

      builder.ConfigureRoot<HomeScreen>(options, "HomeScreens", b =>
      {
        b.HasMany(hs => hs.Franchises)
          .WithOne().HasForeignKey(x => x.HomeScreenId);
      });

      builder.Entity<HomeScreenFranchise>(b =>
      {
        b.ToTable(options.TablePrefix + "HomeScreenFranchises", options.Schema);

        b.ConfigureByConvention();

        b.HasKey(x => new {x.HomeScreenId, x.FranchiseId});
      });

      builder.ConfigureRoot<Branding>(options, "Branding", b =>
      {
        b.OwnsMany(x => x.BulletPoints, bb =>
        {
          bb.ToTable(options.TablePrefix + "BulletPoints", options.Schema);
          bb.HasKey(x => x.Id);
//            bb.OnDelete(DeleteBehavior.Cascade);

          bb.ConfigureByConvention();
          bb.TryConfigureFullAudited();
        });
      });

      builder.ConfigureRoot<Hero>(options, "Heroes");

      builder.ConfigureRoot<TimeZone>(options, "TimeZones");

      builder.ConfigureRoot<Provider>(options, "Providers", b => { b.HasIndex(x => x.Operator); });

      builder.ConfigureRoot<AppUser>(options, "AppUsers", b =>
      {
        b
          .HasIndex(x => x.IdentityUserId)
          .HasName("nci_wi_MovieChecklistAppUsers_CB765B4A48FCEA8EB");

        b.OwnsOne(x => x.Watchlist, wb =>
        {
          wb.HasKey(x => x.Id);

          wb.ToTable(options.TablePrefix + "Watchlist", options.Schema);
//          wb.OnDelete(DeleteBehavior.Cascade);

          wb.ConfigureByConvention();
          wb.TryConfigureFullAudited();

          wb.OwnsMany(x => x.Items, wib =>
            {
              wib.ToTable(options.TablePrefix + "WatchlistEntries", options.Schema);
              wib.HasOne(x => x.Movie).WithMany().HasForeignKey(x => x.MovieId);
              wib.HasKey(x => x.Id);
              wib.ConfigureByConvention();
              wb.TryConfigureFullAudited();
            })
//            .OnDelete(DeleteBehavior.Cascade)
            ;
        });

        b.OwnsOne(x => x.Reminders, rb =>
        {
          rb.ToTable(options.TablePrefix + "Reminders", options.Schema)
            .WithOwner()
            .HasForeignKey(x => x.AppUserId);
//          rb.OnDelete(DeleteBehavior.Cascade);

          rb.ConfigureByConvention();
          rb.TryConfigureFullAudited();

          rb.OwnsMany(x => x.Items, rib =>
          {
            rib.ToTable(options.TablePrefix + "ReminderItems", options.Schema);
            rib.HasOne<Movie>().WithMany().HasForeignKey(x => x.MovieId);
            rib.HasKey(x => x.Id);

            rb.ConfigureByConvention();
            rb.TryConfigureFullAudited();

//              rb.OnDelete(DeleteBehavior.Cascade);
          });
        });

        b.OwnsMany(x => x.Channels, cb =>
        {
          cb.HasKey(x => x.Id);
          cb.ToTable(options.TablePrefix + "UserChannelInfo", options.Schema);
          cb.HasOne<Network>().WithMany().HasForeignKey(x => x.NetworkId);
          cb.ConfigureByConvention();

          cb.WithOwner().HasForeignKey(x => x.AppUserId);
//          cb.OnDelete(DeleteBehavior.Cascade);
        });

        b.OwnsMany(x => x.AcknowledgedFranchises)
          .WithOwner().HasForeignKey(x => x.AppUserId);
//          .OnDelete(DeleteBehavior.Cascade)
        b.ToTable(options.TablePrefix + "UserAcknowledgedFranchises", options.Schema)
          .HasKey(x => x.Id);
      });

      builder.ConfigureRoot<Sponsor>(options, "Sponsors", b =>
      {
        b.OwnsMany(x => x.AdUnits)
          .WithOwner().HasForeignKey(x => x.SponsorId);
//          .OnDelete(DeleteBehavior.Cascade)
        b.ToTable(options.TablePrefix + "SponsorAdUnits", options.Schema)
          .HasKey(x => x.Id);
      });
    }

The ConfigureRoot looks like this:

public static class ModelBuilderExtensions
  {
    public static void ConfigureRoot<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options, string name,
      Action<EntityTypeBuilder<T>> mods = null)
      where T : class, IHasExtraProperties, IAggregateRoot =>
      builder.Entity<T>(b =>
      {
        b.ToTable(options.TablePrefix + name, options.Schema);

        b.ConfigureByConvention();

        (b as EntityTypeBuilder<IFullAuditedObject>)?.ConfigureFullAudited();

        mods?.Invoke(b);
      });

    public static void ConfigureRoot<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options,
      Action<EntityTypeBuilder<T>> mods = null)
      where T : class, IHasExtraProperties, IAggregateRoot =>
      builder.ConfigureRoot(options, GetName<T>(), mods);

    public static void Entity<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options,
      string name, Action<ModelBuilderConfigurationOptions, EntityTypeBuilder<T>> mods = null)
      where T : class =>
      builder.Entity<T>(b =>
      {
        b.ToTable(options.TablePrefix + name, options.Schema);
        b.ConfigureByConvention();
        mods?.Invoke(options, b);
      });

    public static void Entity<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options,
      Action<ModelBuilderConfigurationOptions, EntityTypeBuilder<T>> mods = null)
      where T : class =>
      builder.Entity(options, GetName<T>(), mods);

    public static void Entity<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options,
      string name, Action<EntityTypeBuilder<T>> mods = null)
      where T : class =>
      builder.Entity<T>(b =>
      {
        b.ToTable(options.TablePrefix + name, options.Schema);
        b.ConfigureByConvention();
        mods?.Invoke(b);
      });

    public static void Entity<T>(this ModelBuilder builder,
      ModelBuilderConfigurationOptions options,
      Action<EntityTypeBuilder<T>> mods = null)
      where T : class =>
      builder.Entity(options, GetName<T>(), mods);

    private static string GetName<T>() where T : class => typeof(T).Name.Pluralize();
  }

Stack Trace

System.InvalidOperationException : The type 'MovieCastMember' cannot be marked as owned because a non-owned entity type with the same name already exists.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalModelBuilder.Entity(TypeIdentity& type, ConfigurationSource configurationSource, Nullable`1 shouldBeOwned)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalModelBuilder.Entity(Type type, ConfigurationSource configurationSource, Nullable`1 shouldBeOwned)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalEntityTypeBuilder.HasOwnership(TypeIdentity& targetEntityType, MemberIdentity navigation, Nullable`1 inverse, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalEntityTypeBuilder.HasOwnership(Type targetEntityType, MemberInfo navigationProperty, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.OwnsManyBuilder[TRelatedEntity](MemberIdentity navigation)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.OwnsMany[TRelatedEntity](Expression`1 navigationExpression, Action`1 buildAction)

Further technical details

EF Core version: 3.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer/InMemory Target framework: .NET Core 3.0 Operating system: Manjaro 18.0.4 IDE: Rider 2019.2.2

smitpatel commented 4 years ago

@chris-stormideas - Please share a full repro code which demonstrate the issue. Your code snippets contains too many missing parts for us to effectively reproduce.

ajcvickers commented 4 years ago

EF Team Triage: Closing this issue as the requested additional details have not been provided and we have been unable to reproduce it.

BTW this is a canned response and may have info or details that do not directly apply to this particular issue. While we'd like to spend the time to uniquely address every incoming issue, we get a lot traffic on the EF projects and that is not practical. To ensure we maximize the time we have to work on fixing bugs, implementing new features, etc. we use canned responses for common triage decisions.