我总是将Add-Migration 与连接字符串一起使用,那么如何提供使用它的DbContext 或IDbContextFactory<>?而不是无参数的构造函数?
花了一些时间对实体框架进行逆向工程后,答案是:你不能!
当您运行Add-Migration(没有默认构造函数)时会发生以下情况:
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()
让我们看一下DbMigrator 构造函数。从Add-Migration 命令运行时,usersContext 为空,configuration.TargetDatabasenot 为空并且包含从命令行参数传递的信息,例如-ConnectionStringName、-ConnectionString 和@987654333 @。所以new DbContextInfo(configuration.ContextType, configuration.TargetDatabase) 被调用了。
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);
}
}
// ...
}
为了不抛出 DbMigrator,DbContextInfo 实例必须是可构造的。现在,让我们看看DbContextInfo 构造函数。要使 DbContextInfo 可构造,CreateActivator() 和 CreateInstance() 都不得返回 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 基本上搜索 DbContext 类型或 IDbContextFactory<MyContext> 实现的无参数构造函数并返回 Func<MyContext>。然后CreateInstance 调用该激活器。不幸的是,DbContextInfo 构造函数的DbConnectionInfo connectionInfo 参数没有被激活器使用,而只是在创建上下文实例后才应用(为简洁起见,删除了无关代码):
public virtual DbContext CreateInstance()
{
dbContext = _activator == null ? null : _activator();
dbContext.InternalContext.ApplyContextInfo(this);
return dbContext;
}
然后,在ApplyContextInfo 内部,神奇的事情发生了:连接信息(来自_connectionInfo)在新创建的上下文中被覆盖。
所以,鉴于您必须有一个无参数的构造函数,我的解决方案与您的类似,但有一些更积极的检查。
- 只有在Debug配置中编译时才会添加默认构造函数。
- 如果没有从
Add-Migration 命令调用,默认构造函数将抛出。
这是我的上下文的样子:
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
// ...
}
那我终于可以跑了
Add-Migration -Verbose -ConnectionString "Server=myServer;Database=myDatabase;Integrated Security=SSPI" -ConnectionProviderName "System.Data.SqlClient"
对于运行迁移,我还没有找到明确使用DbMigrator 的解决方案,因此我使用MigrateDatabaseToLatestVersion 数据库初始化程序和useSuppliedContext: true,如How do I inject a connection string into an instance of IDbContextFactory? 中所述。