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.5k stars 3.13k forks source link

Correct Approach to generate ef core 8 migrations programmatically on runtime with previous snapshots #34087

Open aloksharma1 opened 2 weeks ago

aloksharma1 commented 2 weeks ago

Ask a question

Hello, i am trying to setup automatic migrations on runtime for our internal project needs, i am able to generate/migrate correct migrations from dynamically added models on runtime following these issues #6806 #9339 #10872. the problem is i can't generate the correct migrations a second time when there is already some history in __EFMigrationsHistory snapshots, in which case the new migration code and snapshot should avoid existing model in table. But i am always getting same migrations not the differed version.

Include your code

my code is a bit complex, but the scaffolder setup is here:

public static ServiceProvider GetServiceProvider(DbContext context)
{
    Console.WriteLine($"connection provider: {context.Database.ProviderName}");        
    var serviceCollection = new ServiceCollection()
        .AddSingleton(context.GetService<IHistoryRepository>())
        .AddSingleton(context.GetService<IMigrationsIdGenerator>())            
        .AddSingleton(context.GetService<IMigrationsModelDiffer>())
        .AddSingleton(context.GetService<IMigrationsAssembly>)
        .AddSingleton(context.GetService<ICurrentDbContext>())
        .AddSingleton(context.GetService<IDatabaseProvider>())
        .AddSingleton(context.GetService<ITypeMappingSource>())
        .AddSingleton(context.GetService<IDesignTimeModel>())
        .AddSingleton<AnnotationCodeGeneratorDependencies>()
        .AddEntityFrameworkDesignTimeServices()
        .AddDbContextDesignTimeServices(context)
        //.AddSingleton(context.GetInfrastructure().GetRequiredService<IMigrationsScaffolder>()) --> No service for type 'Microsoft.EntityFrameworkCore.Migrations.Design.IMigrationsScaffolder' has been registered
        .AddSingleton<IAnnotationCodeGenerator, AnnotationCodeGenerator>();

    // Register EF Core design-time services
    new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
        .TryAddCoreServices();
    return serviceCollection.BuildServiceProvider();
}

i have also implemented IModelCacheKeyFactory like this:

public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create(DbContext context, bool designTime)
    {
        return context is IDbContextSchema schema
            ? (context.GetType(), schema.Schema, designTime)
            : (object)(context.GetType(), designTime);
    }
    public object Create(DbContext context) => Create(context, false);
}

the db context is here:

public class DynamicContext : DbContext
{
    private readonly IDbContextSchema _schema;

    public DynamicContext(IDbContextSchema schema, DbContextOptions<DynamicContext> options) : base(options)
    {
        _schema = schema;
        Console.Write(Database.ProviderName);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {        
        //optionsBuilder.UseSqlServer("Server=(local);Database=Headless.EFCore;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true");
        optionsBuilder.EnableSensitiveDataLogging().LogTo(Console.WriteLine)
            .ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>()
            .ReplaceService<IMigrationsAssembly, DynamicMigrationsAssembly>();
        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var tableDefinition = Program.TableDefinition;
        if (tableDefinition != null)
        {
            var entity = modelBuilder.Entity(tableDefinition.TableName);
            if (tableDefinition.Columns != null)
            {
                ConfigureRuntimeEntities(modelBuilder, tableDefinition); // create and load classes on runtime using roslyn natasha
            }
        }
        base.OnModelCreating(modelBuilder);
    }
}

Here is my MigrationsAssembly implementation , inspired from this article https://www.thinktecture.com/en/entity-framework-core/changing-db-migration-schema-at-runtime-in-2-1/:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using TypeInfo = System.Reflection.TypeInfo;

[SuppressMessage("Microsoft.EntityFrameworkCore", "EF1001:Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationsAssembly", Justification = "We need Internal Apis for table manipulation")]
public class DynamicMigrationsAssembly : MigrationsAssembly
{
    private List<Assembly> _dynamicAssemblies = new();
    private IReadOnlyDictionary<string, TypeInfo>? _migrations;

    public DynamicMigrationsAssembly(
        ICurrentDbContext currentContext,
        IDbContextOptions options,
        IMigrationsIdGenerator idGenerator,
        IDiagnosticsLogger<DbLoggerCategory.Migrations> logger)
        : base(currentContext, options, idGenerator, logger)
    {
    }

    public override IReadOnlyDictionary<string, TypeInfo> Migrations
    {
        get
        {
            if (_migrations == null)
            {
                var baseMigrations = base.Migrations;
                if (_dynamicAssemblies == null)
                {
                    _migrations = baseMigrations;
                }
                else
                {
                    var dynamicMigrations = _dynamicAssemblies
                        .SelectMany(assembly => assembly.GetTypes()
                            .Where(t => t.IsSubclassOf(typeof(Migration)))
                            .Select(t => new KeyValuePair<string, TypeInfo>(
                                t.GetCustomAttribute<MigrationAttribute>()?.Id ?? t.Name,
                                t.GetTypeInfo())))
                        .ToList();

                    var uniqueMigrations = new Dictionary<string, TypeInfo>(baseMigrations);
                    foreach (var migration in dynamicMigrations)
                    {
                        if (!uniqueMigrations.ContainsKey(migration.Key))
                        {
                            uniqueMigrations.Add(migration.Key, migration.Value);
                        }
                    }

                    _migrations = uniqueMigrations;
                }
            }

            return _migrations;
        }
    }

    public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider)
    {
        var migration = (Migration)Activator.CreateInstance(migrationClass.AsType())!;
        migration.ActiveProvider = activeProvider;

        return migration;
    }
    public DynamicMigrationsAssembly AddNewAssemblies(params Assembly[] newAssemblies)
    {
        _dynamicAssemblies.AddRange(newAssemblies);
        _migrations = null; // Reset migrations cache
        return this;
    }
    public List<Assembly> GetAssemblies()
    {
        return _dynamicAssemblies;
    }
}

I am saving existing migrations as assemblies for future migrations, and i can see it loaded in snapshot model but scaffolder still generates the whole model every time ending in error like Microsoft.Data.SqlClient.SqlException: 'There is already an object named 'Orders' in the database.' . thats the issue.

Include provider and version information

EF Core version: 8.0.6 Database provider: Microsoft.EntityFrameworkCore.SqlServer, want to keep it db agnostic Target framework: .NET 8.0 Operating system: Win 11 IDE: VS Code/ Visual Studio?

aloksharma1 commented 1 week ago

Hello Team, Any update on this issue, fyi i found the possible cause of this issue that looks like ModelSnapshot is null even after loading the existing migration assemblies in memory i checked it using var loadedMigrations = context.GetService<IMigrationsAssembly>();. is there any way to update snapshot manually/programatically after loading the assembly, i had the impression that once we register the old migrations in IMigrationsAssembly implementation it will be taken care of automatically.