Pedro Pedrosa
Pedro Pedrosa

Reputation: 462

How can I use the Entity Framework Core capabilities to change my database?

I am building an application on top of Entity Framework Core and I want to, sort of, apply a migration at runtime.

My intended approach is to have the current database model in memory and create a new model, then calculate the difference between the two models using IMigrationsModelDiffer.GetDifferences().

From there, instead of printing the differences into a Migration class, I want to create the MigrationCommands directly and apply those commands to my database.

The above sounds fairly straightforward but I'm having a lot of issues with the Dependency Injection system.

This is the code I have right now:

static DbContextOptions GetOptions(IModel model)
{
    var builder = new DbContextOptionsBuilder();
    builder
        .UseSqlServer(connStr)
        .UseModel(model);
    return builder.Options;
}
class Test1ModelAEntity
{
    public int Id { get; set; }
    public string StrProp { get; set; }
}
static void Main(string[] args)
{
    var sqlServerServices = new ServiceCollection()
        .AddEntityFrameworkSqlServer()
        .BuildServiceProvider();
    var conventions = new ConventionSet();
    sqlServerServices.GetRequiredService<IConventionSetBuilder>().AddConventions(conventions);

    var emptyModelBuilder = new ModelBuilder(conventions);
    var emptyModel = emptyModelBuilder.Model;

    var test1ModelBuilder = new ModelBuilder(conventions);
    test1ModelBuilder.Entity<Test1ModelAEntity>()
        .ToTable("ModelA");
    var test1Model = test1ModelBuilder.Model;

    using (TestContext ctx = new TestContext(GetOptions(emptyModel)))
    {
        var migrationServices = new ServiceCollection()
            .AddDbContextDesignTimeServices(ctx)
            .AddEntityFrameworkSqlServer()
            .BuildServiceProvider();
        var operations = migrationServices.GetRequiredService<IMigrationsModelDiffer>().GetDifferences(emptyModel, test1Model);
        var commands = migrationServices.GetRequiredService<IMigrationsSqlGenerator>().Generate(operations, test1Model);
        var connection = migrationServices.GetRequiredService<IRelationalConnection>();
        migrationServices.GetRequiredService<IMigrationCommandExecutor>().ExecuteNonQuery(commands, connection);
    }
}

This code throws a NullReferenceException with this stack trace:

at Microsoft.EntityFrameworkCore.Metadata.Internal.TableMapping.<>c.<GetRootType>b__10_0(IEntityType t)
at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.GetSortedProperties(TableMapping target)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.<Add>d__37.MoveNext()
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.<DiffCollection>d__73`1.MoveNext()
at System.Linq.Enumerable.ConcatIterator`1.MoveNext()
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsModelDiffer.Sort(IEnumerable`1 operations, DiffContext diffContext)
at Sandbox.Program.Main(String[] args) in D:\Sandbox\Program.cs:line 108

I have inspected the source code and it appears that there's an issue with the way EFCore is interpreting my model. I am using EFCore version 2.1 preview 2.

Really I'm mostly trying random configurations on my IServiceCollections because I have no idea how to set this up. I am also trying to stay away from EFCore internal classes but if needed be I may use one or two for the time being.

Is there a way to take advantage of EFCore's built-in capabilities to generate some SQL given a pair of IModels? If so, how do I set up DI to have all the required services?

Upvotes: 3

Views: 1970

Answers (1)

Pedro Pedrosa
Pedro Pedrosa

Reputation: 462

Thank you for the comments which pointed me in the correct direction.

In summary, I was trying to create my models using an empty convention set. This obviously leads to all sorts of problems as you have to generate the entire model explicitly, which is very complex.

To use the expected convention set I had to get it from my context using ConventionSet.CreateConventionSet. I also had to manually validate my model before being able to use it in queries and insert commands. The rest of the logic is pretty much the same.

Here's my final code including the tests I ran to ensure everything worked as expected:

static DbContextOptions GetOptions(IModel model)
{
    var builder = new DbContextOptionsBuilder();
    builder
        .UseSqlServer(connStr)
        .UseModel(model);
    return builder.Options;
}

//Test 1
class Test1EntityA
{
    public int Id { get; set; }
    public string StrProp { get; set; }
}

//Test 2
class Test2EntityA
{
    public int Id { get; set; }
    public string StrProp { get; set; }
    public ICollection<Test2ModelBEntity> Children { get; set; }
}
class Test2EntityB
{
    public int Id { get; set; }
    public int EntityAId { get; set; }
    public Test2EntityA EntityA { get; set; }
}

static void Main(string[] args)
{
    var emptyModelBuilder = new ModelBuilder(new ConventionSet());
    var emptyModel = emptyModelBuilder.Model;
    using (var baseCtx = new TestContext(GetOptions(emptyModel)))
    {
        //Get all services we need from the base context
        var conventions = ConventionSet.CreateConventionSet(baseCtx);
        var migrationServices = new ServiceCollection()
            .AddDbContextDesignTimeServices(baseCtx)
            .AddEntityFrameworkSqlServer()
            .BuildServiceProvider();

        //Test 1
        var test1ModelBuilder = new ModelBuilder(conventions);
        test1ModelBuilder.Entity<Test1EntityA>()
            .ToTable("EntityA");
        var test1Model = test1ModelBuilder.GetInfrastructure().Metadata;
        test1Model.Validate();

        var operations = migrationServices.GetRequiredService<IMigrationsModelDiffer>().GetDifferences(emptyModel, test1Model);
        var commands = migrationServices.GetRequiredService<IMigrationsSqlGenerator>().Generate(operations, test1Model);
        var connection = migrationServices.GetRequiredService<IRelationalConnection>();
        migrationServices.GetRequiredService<IMigrationCommandExecutor>().ExecuteNonQuery(commands, connection);

        using (TestContext ctx = new TestContext(GetOptions(test1Model)))
        {
            ctx.Set<Test1EntityA>().Add(new Test1EntityA
            {
                StrProp = "test1",
            });
            ctx.SaveChanges();
        }

        //Test 2
        var test2ModelBuilder = new ModelBuilder(conventions);
        test2ModelBuilder.Entity<Test2EntityA>()
            .ToTable("EntityA");
        test2ModelBuilder.Entity<Test2EntityB>()
            .ToTable("EntityB");
        var test2Model = test2ModelBuilder.GetInfrastructure().Metadata;
        test2Model.Validate();

        operations = migrationServices.GetRequiredService<IMigrationsModelDiffer>().GetDifferences(test1Model, test2Model);
        commands = migrationServices.GetRequiredService<IMigrationsSqlGenerator>().Generate(operations, test2Model);
        migrationServices.GetRequiredService<IMigrationCommandExecutor>().ExecuteNonQuery(commands, connection);

        using (TestContext ctx = new TestContext(GetOptions(test2Model)))
        {
            var e = new Test2EntityA
            {
                StrProp = "test2",
            };
            ctx.Set<Test2EntityA>().Add(e);
            ctx.Set<Test2EntityB>().Add(new Test2EntityB
            {
                EntityA = e,
            });
            ctx.SaveChanges();

            Console.WriteLine(ctx.Set<Test2EntityB>().Where(b => b.EntityA.StrProp == "test2").Count());
        }
    }
}

Upvotes: 4

Related Questions