Add-Migration without parameterless DbContext and DbContextFactory constructor

早过忘川 提交于 2021-02-08 14:15:19

问题


My application has no parameterless constructor at my DbContext implementation and I don't like to provide a parameterless constructor to a IDbContextFactory<> implementation.

The reason is I want to keep control where the DbContext points to. That's why all my constructors will ask for the ConnectionStringProvider.

public class MyDbContext : DbContext
{
    internal MyDbContext(IConnectionStringProvider provider) : base(provider.ConnectionString) {}
}

and

public class MyContextFactory : IDbContextFactory<MyDbContext>
{
    private readonly IConnectionStringProvider _provider;
    public MyContextFactory(IConnectionStringProvider provider)
    {
        _provider = provider;
    }
    public MyDbContext Create()
    {
        return new MyDbContext(_provider.ConnectionString);
    }
}

I definitely don’t want to add a default constructor! I already did that and it crashed on production because of the wrong connection strings inside the wrong App.config or assuming a default connection string like the default constructor of DbContext does. I would like use the same infrastructure on

  • Debug/Relase (and only inject a different IConnectionStringProvider)
  • Calling Add-Migration script
  • Running DbMigrator.GetPendingMigrations()

Currently I get some of those messages:

The context factory type 'Test.MyContextFactory' does not have a public parameterless constructor. Either add a public parameterless constructor, create an IDbContextFactory implementation in the context assembly, or register a context factory using DbConfiguration.

---UPDATE---

This might be a duplicate of How do I inject a connection string into an instance of IDbContextFactory<T>? but it has no solution. I explain why:

  • I always use Add-Migration with connection string, so how can I provide a DbContext or IDbContextFactory<> that consumes it? Instead of parameterless constructors?

    Add-Migration MyMigration -ConnectionStringName "MyConnectionString"

  • The same problem here: I use DbMigrator.GetPendingMigrations() which also asks for parameterless DbContext or IDbContextFactory<> implementations.

As far as I understand EntityFramework violates encapsulation by implying default constructors and causes temporal coupling which is not fail-safe. So please propose a solution without parameterless constructors.


回答1:


I always use Add-Migration with connection string, so how can I provide a DbContext or IDbContextFactory<> that consumes it? Instead of parameterless constructors?

After spending some time reverse-engineering Entity Framework it turns out the answer is: you can't!

Here's what happens when you run Add-Migration (with no default constructor):

System.Data.Entity.Migrations.Infrastructure.MigrationsException: The target context 'Namespace.MyContext' is not constructible. Add a default constructor or provide an implementation of IDbContextFactory.
   at System.Data.Entity.Migrations.DbMigrator..ctor(DbMigrationsConfiguration configuration, DbContext usersContext, DatabaseExistenceState existenceState, Boolean calledByCreateDatabase)
   at System.Data.Entity.Migrations.DbMigrator..ctor(DbMigrationsConfiguration configuration)
   at System.Data.Entity.Migrations.Design.MigrationScaffolder..ctor(DbMigrationsConfiguration migrationsConfiguration)
   at System.Data.Entity.Migrations.Design.ToolingFacade.ScaffoldRunner.RunCore()
   at System.Data.Entity.Migrations.Design.ToolingFacade.BaseRunner.Run()

Let's have a look at the DbMigrator constructor. When run from the Add-Migration command, usersContext is null, configuration.TargetDatabase is not null and contains information passed from the command-line parameters such as -ConnectionStringName, -ConnectionString and -ConnectionProviderName. So new DbContextInfo(configuration.ContextType, configuration.TargetDatabase) is called.

internal DbMigrator(DbMigrationsConfiguration configuration, DbContext usersContext, DatabaseExistenceState existenceState, bool calledByCreateDatabase) : base(null)
{
    Check.NotNull(configuration, "configuration");
    Check.NotNull(configuration.ContextType, "configuration.ContextType");
    _configuration = configuration;
    _calledByCreateDatabase = calledByCreateDatabase;
    _existenceState = existenceState;
    if (usersContext != null)
    {
        _usersContextInfo = new DbContextInfo(usersContext);
    }
    else
    {
        _usersContextInfo = ((configuration.TargetDatabase == null) ?
            new DbContextInfo(configuration.ContextType) :
            new DbContextInfo(configuration.ContextType, configuration.TargetDatabase));
        if (!_usersContextInfo.IsConstructible)
        {
            throw Error.ContextNotConstructible(configuration.ContextType);
        }
    }
    // ...
}

For the DbMigrator not to throw, the DbContextInfo instance must be constructible. Now, let's look at the DbContextInfo constructor. For the DbContextInfo to be constructible, both CreateActivator() and CreateInstance() must not return null.

private DbContextInfo(Type contextType, DbProviderInfo modelProviderInfo, AppConfig config, DbConnectionInfo connectionInfo, Func<IDbDependencyResolver> resolver = null)
{
    _resolver = (resolver ?? ((Func<IDbDependencyResolver>)(() => DbConfiguration.DependencyResolver)));
    _contextType = contextType;
    _modelProviderInfo = modelProviderInfo;
    _appConfig = config;
    _connectionInfo = connectionInfo;
    _activator = CreateActivator();
    if (_activator != null)
    {
        DbContext dbContext = CreateInstance();
        if (dbContext != null)
        {
            _isConstructible = true;
            using (dbContext)
            {
                _connectionString = DbInterception.Dispatch.Connection.GetConnectionString(dbContext.InternalContext.Connection, new DbInterceptionContext().WithDbContext(dbContext));
                _connectionStringName = dbContext.InternalContext.ConnectionStringName;
                _connectionProviderName = dbContext.InternalContext.ProviderName;
                _connectionStringOrigin = dbContext.InternalContext.ConnectionStringOrigin;
            }
        }
    }
    public virtual bool IsConstructible => _isConstructible;
}

CreateActivator basically searches for a parameterless constructor of either your DbContext type or your IDbContextFactory<MyContext> implementation and returns a Func<MyContext>. Then CreateInstance calls that activator. Unfortunately, the DbConnectionInfo connectionInfo parameter of the DbContextInfo constructor is not used by the activator but is only applied later after the context instance is created (irrelevant code removed for brevity):

public virtual DbContext CreateInstance()
{
    dbContext = _activator == null ? null : _activator();
    dbContext.InternalContext.ApplyContextInfo(this);
    return dbContext;
}

Then, inside ApplyContextInfo, the magic happens: the connection info (from _connectionInfo) is overridden on the newly created context.

So, given that you must have a parameterless constructor, my solution is similar to yours, but with a few more aggressive checks.

  1. The default constructor is only added when compiling in Debug configuration.
  2. The default constructor throws if not called from the Add-Migration command.

Here's what my context look like:

public class MyContext : DbContext
{
    static MyContext()
    {
        System.Data.Entity.Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyContextConfiguration>(useSuppliedContext: true));
    }

#if DEBUG
    public MyContext()
    {
        var stackTrace = new System.Diagnostics.StackTrace();
        var isMigration = stackTrace.GetFrames()?.Any(e => e.GetMethod().DeclaringType?.Namespace == typeof(System.Data.Entity.Migrations.Design.ToolingFacade).Namespace) ?? false;
        if (!isMigration)
            throw new InvalidOperationException($"The {GetType().Name} default constructor must be used exclusively for running Add-Migration in the Package Manager Console.");
    }
#endif
    // ...
}

Then I can finally run

Add-Migration -Verbose -ConnectionString "Server=myServer;Database=myDatabase;Integrated Security=SSPI" -ConnectionProviderName "System.Data.SqlClient"

And for running the migrations I haven't found a solution using a DbMigrator explicitly, so I use the MigrateDatabaseToLatestVersion database initializer with useSuppliedContext: true as explained in How do I inject a connection string into an instance of IDbContextFactory? .




回答2:


Ok, I guess there's no answer!

That's why I'd like to announce my stomach aching workaround: As there's no way to get rid of the default constructor (and satisfy principles of encapsulation) I provide an empty constructor with an intentionally false connection string. So in case it will be used for anything else than migration it fails on runtime as early as possible and in all enviroments (Debug/Integration/Release).

public class MyDbContextFactory : IDbContextFactory<MyDbContext>
{
    private readonly string _connectionString;

    public MyDbContextFactory(string connectionString)
    {
        _connectionString = connectionString;
    }

    public MyDbContextFactory()
    {
        _connectionString = "MIGRATION_ONLY_DONT_USE_ITS_FAKE!";
    }

    public MyDbContext Create()
    {
        return new MyDbContext(_connectionString);
    }
}

(I wouldn't consider this as an answer so feel free to post a better solution.)




回答3:


Create a Migrate Initializer that takes a connection string as constructur parameter then you could pass it to the Migration Constructor so it can use that connection string

 public class MigrateInitializer : MigrateDatabaseToLatestVersion<MyContext, Configuration>
    {
        public MigrateInitializer(string connectionString) : base(true, new Configuration() { TargetDatabase=new  System.Data.Entity.Infrastructure.DbConnectionInfo(connectionString,"System.Data.SqlClient") })
        {
        }

    }

The pass it to the MigrateInitializer

public class MyContext : DbContext { public MyContext(string connectionString) : base(connectionString) { Database.SetInitializer(new MigrateInitializer(connectionString)); }

}

Thats it now the migration will use the connection string your provided




回答4:


Another solution would be to migrate to Entity Framework Core. They have thought about that issue and there's a IDesignTimeDbContextFactory.CreateDbContext(string[] args) interface where args are the arguments provided by the design-time service.

But beware, as of Entity Framework Core 2.1 this feature is not yet implemented! See Design-time DbContext Creation for the documentation and Tools: Flow arguments into IDesignTimeDbContextFactory on GitHub to follow the progress and be notified when this will be implemented.



来源:https://stackoverflow.com/questions/42296196/add-migration-without-parameterless-dbcontext-and-dbcontextfactory-constructor

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!