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.
- The default constructor is only added when compiling in Debug configuration.
- 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? .