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

ToView for entity types with owned types throws NRE #18298

Closed
TAGC opened this issue Oct 9, 2019 · 6 comments · Fixed by #18495
Closed

ToView for entity types with owned types throws NRE #18298

TAGC opened this issue Oct 9, 2019 · 6 comments · Fixed by #18495
Labels
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

@TAGC
Copy link

TAGC commented Oct 9, 2019

Given a domain consisting of two entities X and Y where X owns an instance of Y and both are mapped to a view, any attempt made to create an initial database migration using Add-Migration or dotnet ef migration add fails with a NullReferenceException.

Steps to reproduce

  1. Create a .NET Core 3 console application (Experiment).
  2. Install Microsoft.EntityFrameworkCore.Tools in Experiment.
  3. Create a .NET Standard 2.1 C# library (Experiment.Lib).
  4. Install Microsoft.EntityFrameworkCore.SqlServer in Experiment.Lib.
  5. Create these classes in Experiment.Lib.
  6. Reference Experiment.Lib from Experiment.
  7. In Visual Studio, open the Package Manager Console and set the default project to Experiment.Lib, then run Add-Migration InitialCreate.

Actual Result

The following exception is generated:

PM> Add-Migration InitialCreate -Verbose
Using project 'Experiment.Lib'.
Using startup project 'Experiment'.
Build started...
Build succeeded.
C:\Program Files\dotnet\dotnet.exe exec --depsfile C:\Projects\Experiment\Experiment\bin\Debug\netcoreapp3.0\Experiment.deps.json --additionalprobingpath C:\Users\e0065186\.nuget\packages --additionalprobingpath "C:\Program Files\dotnet\sdk\NuGetFallbackFolder" --runtimeconfig C:\Projects\Experiment\Experiment\bin\Debug\netcoreapp3.0\Experiment.runtimeconfig.json C:\Users\e0065186\.nuget\packages\microsoft.entityframeworkcore.tools\3.0.0\tools\netcoreapp2.0\any\ef.dll migrations add InitialCreate --json --verbose --no-color --prefix-output --assembly C:\Projects\Experiment\Experiment\bin\Debug\netcoreapp3.0\Experiment.Lib.dll --startup-assembly C:\Projects\Experiment\Experiment\bin\Debug\netcoreapp3.0\Experiment.dll --project-dir C:\Projects\Experiment\Experiment.Lib\ --language C# --working-dir C:\Projects\Experiment --root-namespace Experiment.Lib
Using assembly 'Experiment.Lib'.
Using startup assembly 'Experiment'.
Using application base 'C:\Projects\Experiment\Experiment\bin\Debug\netcoreapp3.0'.
Using working directory 'C:\Projects\Experiment\Experiment'.
Using root namespace 'Experiment.Lib'.
Using project directory 'C:\Projects\Experiment\Experiment.Lib\'.
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'BranchContext'.
Using context 'BranchContext'.
Finding design-time services for provider 'Microsoft.EntityFrameworkCore.SqlServer'...
Using design-time services from provider 'Microsoft.EntityFrameworkCore.SqlServer'.
Finding design-time services referenced by assembly 'Experiment'.
No referenced design-time services were found.
Finding IDesignTimeServices implementations in assembly 'Experiment'...
No design-time services were found.
'BranchContext' disposed.
System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.MetadataExtensions.AsConcreteMetadataType[TInterface,TConcrete](TInterface interface, String methodName)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityTypeExtensions.AsEntityType(IEntityType entityType, String methodName)
   at Microsoft.EntityFrameworkCore.EntityTypeExtensions.GetDeclaredProperties(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetSortedProperties(IEntityType entityType)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetSortedProperties(TableMapping target)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Add(TableMapping target, DiffContext diffContext)+MoveNext()
   at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
   at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext)
   at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetDifferences(IModel source, IModel target)
   at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Object reference not set to an instance of an object.

Comment/uncomment the other lines in BranchContext:OnModelCreating() to confirm that this does not occur with some minor changes to the entity configuration logic.

Expected Result

A migration should be generated to create a view on the database that will contain columns that correspond to the properties of Order and Vehicle.

Further technical details

EF Core version: EF Core 3.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET Core 3
Operating system: Windows 10 64-bit
IDE: Visual Studio 2019 16.3

@ajcvickers
Copy link
Member

@TAGC A keyless entity type cannot own another entity type because this implies that the owned type borrows the key of the owner...which doesn't have one.

@AndriySvyryd
Copy link
Member

We should throw a better exception until #14497 is implemented.

@AndriySvyryd AndriySvyryd reopened this Oct 14, 2019
@TAGC
Copy link
Author

TAGC commented Oct 14, 2019

The thing is that I've got a custom DbContext that's similar to this and it works fine for actually performing queries against the database. It's just the actual generation of the migration (to make it easy to recreate the database views if needed) that's problematic.

I've encountered exceptions about keyless entity types not being able to own other entity types in the past, but those were very clear exceptions, and they occurred when trying to perform the actual queries, not generating migrations. This is different.

I could try playing around with that SSCCE to see if I can run queries without it throwing exceptions.


As an aside, in my production app I've also realised that I've modelled the domain logic incorrectly - specifically, that my assumption that there's a one-to-one mapping between two entities was wrong. This means that I no longer model any entity as owning another entity. I've just tried generating a migration using the EF Core command-line tool and it succeeds, but the migration logic is empty:

using Microsoft.EntityFrameworkCore.Migrations;

namespace MyCompany.MyProduct.Infrastructure.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {

        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {

        }
    }
}

I assume that this is expected behaviour since I guess EF Core won't know what exact SQL is necessary to generate the database views. If so, that's not an issue and I'll be able to manually write that logic.

@AndriySvyryd AndriySvyryd changed the title Unable to create migration when configuring keyless entity to own another entity Improve the exception when configuring keyless entity to own another entity Oct 15, 2019
@TAGC
Copy link
Author

TAGC commented Oct 16, 2019

Just a quick update on what I've found through playing around with this toy project.

I've found that explicitly configuring the primary key for the entities allows migrations to be generated. I've updated the gist to reflect this approach, but this is the relevant part:

private static void MapToViewWithKeys(EntityTypeBuilder<Order> builder)
{
    builder.ToView("vw_OrderStatus_Orders");
    builder.HasKey(o => o.OrderId);
    builder.OwnsOne(o => o.Vehicle, vb => vb.HasKey(v => v.Id));
}

However, the migration that gets generated attempts to create a table rather than a view. I'm not sure if this is intended behaviour:

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "vw_OrderStatus_Orders1",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                OrderId = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_vw_OrderStatus_Orders1", x => x.Id);
                table.ForeignKey(
                    name: "FK_vw_OrderStatus_Orders1_vw_OrderStatus_Orders_OrderId",
                    column: x => x.OrderId,
                    principalTable: "vw_OrderStatus_Orders",
                    principalColumn: "OrderId",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateIndex(
            name: "IX_vw_OrderStatus_Orders1_OrderId",
            table: "vw_OrderStatus_Orders1",
            column: "OrderId",
            unique: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "vw_OrderStatus_Orders1");
    }
}

I've also noticed that the publicly exposed DbSets influences the migration that gets generated. For example, uncommenting the DbSet<Vehicle> property for BranchContext.cs in the updated version of the gist causes this migration to be generated instead:

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Vehicles",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                OrderId = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Vehicles", x => x.Id);
                table.ForeignKey(
                    name: "FK_Vehicles_vw_OrderStatus_Orders_OrderId",
                    column: x => x.OrderId,
                    principalTable: "vw_OrderStatus_Orders",
                    principalColumn: "OrderId",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateIndex(
            name: "IX_Vehicles_OrderId",
            table: "Vehicles",
            column: "OrderId",
            unique: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Vehicles");
    }
}

@smitpatel
Copy link
Contributor

@AndriySvyryd @bricelam - I tested this and there are weird issues.

  1. Define a keyless entity and map it to view. Works as expected and creates empty migration.
  2. Define PK for above and map it to view. Again empty migration.
  3. Map an entity to view and define key manually. Configure owned entity for this. It throws exception as OP. In this case, I am configuring key entity to own another one yet the exception.
  4. As in 3 but also configure PK for owned entity manually. And it generates migrations which is mismatch on various things.
    public class Blog
    {
        public int Count { get; set; }
        public Address Address { get; set; }

    }

    public class Address
    {
        public string City { get; set; }
    }

        public DbSet<Blog> Blogs { get; set; }
// OnModelCreating
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Configure model
            modelBuilder.Entity<Blog>().ToView("A");
            modelBuilder.Entity<Blog>().HasKey(e => e.Count);
            modelBuilder.Entity<Blog>().OwnsOne(e => e.Address, ow => ow.HasKey(e => e.City));
        }

Migration.
What is A1 table?

public partial class Init : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "A1",
                columns: table => new
                {
                    City = table.Column<string>(nullable: false),
                    BlogCount = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_A1", x => x.City);
                    table.ForeignKey(
                        name: "FK_A1_A_BlogCount",
                        column: x => x.BlogCount,
                        principalTable: "A",
                        principalColumn: "Count",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_A1_BlogCount",
                table: "A1",
                column: "BlogCount",
                unique: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "A1");
        }
    }

Model snapshot. Empty.

[DbContext(typeof(MyContext))]
    partial class MyContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasAnnotation("ProductVersion", "3.1.0-preview2.19501.6")
                .HasAnnotation("Relational:MaxIdentifierLength", 128)
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
#pragma warning restore 612, 618
        }
    }

@ajcvickers
Copy link
Member

Filed #18458 for the migrations part of this.

@ajcvickers ajcvickers added this to the 3.1.0 milestone Oct 18, 2019
@AndriySvyryd AndriySvyryd changed the title Improve the exception when configuring keyless entity to own another entity ToView for entity types with owned types throws NRE Oct 21, 2019
@AndriySvyryd AndriySvyryd removed their assignment Oct 21, 2019
@AndriySvyryd AndriySvyryd added type-bug closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed type-enhancement labels Oct 21, 2019
@ajcvickers ajcvickers modified the milestones: 3.1.0, 3.1.0-preview2 Oct 24, 2019
@ajcvickers ajcvickers modified the milestones: 3.1.0-preview2, 3.1.0 Dec 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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
Development

Successfully merging a pull request may close this issue.

4 participants