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.68k stars 3.17k forks source link

IDesignTimeDbContextFactory with UseModel produces incorrect model #26802

Closed hemirunner426 closed 3 weeks ago

hemirunner426 commented 2 years ago

Consider this model:

public class TestModel
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long Id { get; set; }
        public string? Name { get; set; }
    }

Along with a design time DbContext factory:

internal class TestDbDesignContext : IDesignTimeDbContextFactory<TestContext>
    {
        public TestContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(@"Server=localhost;Database=TestDB;Trusted_Connection=True;");
            var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
            modelBuilder.Entity<TestModel>();
            optionsBuilder.UseModel((IModel)modelBuilder.Model);
            return new TestContext(optionsBuilder.Options);
        }
    }

Running dotnet ef migrations add Initial produces the following migration:

public partial class Initial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "TestModel",
                columns: table => new
                {
                    Id = table.Column<long>(type: "bigint", nullable: false),
                    Name = table.Column<string>(type: "nvarchar(max)", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_TestModel", x => x.Id);
                });
        }

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

In previous versions of EF core the ID colum would be appended like the following:

Id = table.Column<long>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),

When the EF 6 migration is applied to the database the ID column is not set as Identity = True. Thus, on insert the DbContext generates the following exception:

Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
 ---> Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot insert the value NULL into column 'Id', table TestDB.dbo.TestModel'; column does not allow nulls. INSERT fails.

EF Core version: 6.0.0 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0 Operating system: Windows 11 IDE: Visual Studio 2022

ajcvickers commented 2 years ago

@AndriySvyryd This seems to be caused by building the model externally from the context:

var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
modelBuilder.Entity<TestModel>();
optionsBuilder.UseModel((IModel)modelBuilder.Model);

Explicitly finalizing the model doesn't help:

var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
modelBuilder.Entity<TestModel>();
var finalizeModel = modelBuilder.FinalizeModel();
optionsBuilder.UseModel(finalizeModel);

@hemirunner426 As a workaround, configure the model in OnModelCreating.

hemirunner426 commented 2 years ago

@ajcvickers Thanks for the suggestion I'll take a look to see if I can implement this workaround at design time in my production application.

AndriySvyryd commented 2 years ago

This is a consequence of the model initialization breaking change

The full fix is:

       public TestContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(@"Server=localhost;Database=TestDB;Trusted_Connection=True;");
            var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
            modelBuilder.Entity<TestModel>();

            var model = modelBuilder.Model.FinalizeModel();
            var serviceContext = new TestContext(optionsBuilder.Options);
            model = serviceContext.GetService<IModelRuntimeInitializer>().Initialize(model);

            optionsBuilder.UseModel(model);

            return new TestContext(optionsBuilder.Options);
        }

We could add a check and throw an exception when this scenario is detected.

hemirunner426 commented 2 years ago

This is a consequence of the model initialization breaking change

The full fix is:

       public TestContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder();
            optionsBuilder.UseSqlServer(@"Server=localhost;Database=TestDB;Trusted_Connection=True;");
            var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
            modelBuilder.Entity<TestModel>();

            var model = modelBuilder.Model.FinalizeModel();
            var serviceContext = new TestContext(optionsBuilder.Options);
            model = serviceContext.GetService<IModelRuntimeInitializer>().Initialize(model);

            optionsBuilder.UseModel(model);

            return new TestContext(optionsBuilder.Options);
        }

We could add a check and throw an exception when this scenario is detected.

This workaround seemed to work. Thanks.

AndriySvyryd commented 2 years ago

Fixing https://github.com/dotnet/efcore/issues/26186 would also fix this.

ajcvickers commented 2 years ago

Note from triage: fixing this is complicated/risky. For now, we will document how to create an appropriate model to pass to UseModel.

@AndriySvyryd do we have a docs issue tracking documentation of this as a breaking change?

AndriySvyryd commented 2 years ago

@ajcvickers It's already documented https://github.com/dotnet/EntityFramework.Docs/blob/main/entity-framework/core/what-is-new/ef-core-6.0/breaking-changes.md#snapshot-initialization Just hasn't been pushed to live

delikelli commented 1 year ago

Hi! @AndriySvyryd . why do we have to create two instance from db context? another problem is we have a seperation with db context class and its dbcontextoptionsbuilder class and they are on different class libraries so we can not create object for dbcontext at method that uses model builder operations. or do you have a different suggestion for design time db context factoryr model producing?

AndriySvyryd commented 1 year ago

@delikelli What is the reason for the dbcontextoptionsbuilder-related code to be in a different assembly? Perhaps there's a way of refactoring out the model building code so it can be also used for the design-time factory

AndriySvyryd commented 3 weeks ago

Fixed in 9e7002c