【问题标题】:Add-Migration without parameterless DbContext and DbContextFactory constructor没有无参数 DbContext 和 DbContextFactory 构造函数的添加迁移
【发布时间】:2017-02-17 10:59:36
【问题描述】:

我的应用程序在我的DbContext 实现中没有无参数构造函数,我不喜欢为IDbContextFactory<> 实现提供无参数构造函数。

原因是我想控制 DbContext 指向的位置。这就是为什么我所有的构造函数都会要求 ConnectionStringProvider。

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

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

我绝对不想添加默认构造函数!我已经这样做了,它在生产中崩溃了,因为错误的 App.config 中的错误连接字符串或假设默认连接字符串就像DbContext 的默认构造函数一样。我想在

上使用相同的基础架构
  • 调试/发布(并且只注入不同的IConnectionStringProvider
  • 调用Add-Migration脚本
  • 正在运行DbMigrator.GetPendingMigrations()

目前我收到了其中一些消息:

上下文工厂类型“Test.MyContextFactory”没有公共无参数构造函数。添加一个公共无参数构造函数,在上下文程序集中创建一个 IDbContextFactory 实现,或者使用 DbConfiguration 注册一个上下文工厂。

---更新---

这可能与How do I inject a connection string into an instance of IDbContextFactory<T>? 重复,但它没有解决方案。我解释原因:

  • 我总是将Add-Migration 与连接字符串一起使用,那么我如何提供使用它的DbContextIDbContextFactory&lt;&gt;?而不是无参数构造函数?

    添加迁移 MyMigration -ConnectionStringName "MyConnectionString"

  • 同样的问题在这里:我使用DbMigrator.GetPendingMigrations(),它还要求无参数的DbContextIDbContextFactory&lt;&gt; 实现。

据我了解 EntityFramework 违反封装 by implying default constructors 和原因 temporal coupling 这不是故障安全的。所以请提出一个没有无参数构造函数的解决方案。

【问题讨论】:

  • 您可能需要考虑使用构造函数注入的外观(也就是静态服务定位器提供的默认值):blog.ploeh.dk/2014/05/19/di-friendly-library
  • @AndreasNiedermair 感谢您的评论。但据我了解,它需要一个无参数的构造函数。那不是我想要的。我想让Add-Migration 或至少DbMigrator.GetPendingMigrations() 在没有无参数构造函数的情况下运行。因为我无法提供默认值。
  • 您必须在某个时候为参数提供一些值 - 这将是默认值,由静态服务定位器解析,需要特定的注入/注册。无论如何,这个解决方案比 good 解决方案更像是一种解决方法...... :)
  • @AndreasNiedermair 否。标记的解决方案在没有构造函数的情况下实现了IDbContextFactory&lt;DataStore&gt;,因此默认(无参数)构造函数仍然存在。

标签: c# entity-framework-6


【解决方案1】:

我总是将Add-Migration 与连接字符串一起使用,那么如何提供使用它的DbContextIDbContextFactory&lt;&gt;?而不是无参数的构造函数?

花了一些时间对实体框架进行逆向工程后,答案是:你不能!

当您运行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);
        }
    }
    // ...
}

为了不抛出 DbMigratorDbContextInfo 实例必须是可构造的。现在,让我们看看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&lt;MyContext&gt; 实现的无参数构造函数并返回 Func&lt;MyContext&gt;。然后CreateInstance 调用该激活器。不幸的是,DbContextInfo 构造函数的DbConnectionInfo connectionInfo 参数没有被激活器使用,而只是在创建上下文实例后才应用(为简洁起见,删除了无关代码):

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

然后,在ApplyContextInfo 内部,神奇的事情发生了:连接信息(来自_connectionInfo)在新创建的上下文中被覆盖。

所以,鉴于您必须有一个无参数的构造函数,我的解决方案与您的类似,但有一些更积极的检查。

  1. 只有在Debug配置中编译时才会添加默认构造函数。
  2. 如果没有从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? 中所述。

【讨论】:

    【解决方案2】:

    好吧,我猜没有答案!

    这就是为什么我要宣布我的胃痛解决方法:由于无法摆脱默认构造函数 (and satisfy principles of encapsulation),我提供了一个带有故意错误连接字符串的空构造函数.因此,如果它将用于迁移以外的任何其他事情,它会尽早在运行时和所有环境(调试/集成/发布)中失败。

    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);
        }
    }
    

    (我不认为这是一个答案,所以请随时发布更好的解决方案。)

    【讨论】:

      【解决方案3】:

      创建一个将连接字符串作为构造参数的迁移初始化程序,然后您可以将其传递给迁移构造函数,以便它可以使用该连接字符串

       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") })
              {
              }
      
          }
      

      将其传递给 MigrateInitializer

      公共类 MyContext : DbContext { 公共 MyContext(字符串连接字符串) :基础(连接字符串) { Database.SetInitializer(new MigrateInitializer(connectionString)); }

      }

      现在迁移将使用您提供的连接字符串

      【讨论】:

        【解决方案4】:

        另一个解决方案是迁移到 Entity Framework Core。他们已经考虑过这个问题,并且有一个IDesignTimeDbContextFactory.CreateDbContext(string[] args) 接口,其中args 是设计时服务提供的参数。

        但请注意,从 Entity Framework Core 2.1 开始,此功能尚未实现!有关文档,请参阅 Design-time DbContext Creation 和 GitHub 上的 Tools: Flow arguments into IDesignTimeDbContextFactory 以跟踪进度并在实施时收到通知。

        【讨论】:

          【解决方案5】:

          扩展答案https://stackoverflow.com/a/53778826/9941549:该功能终于实现了,可以与 EfCore.Design 5.x 包一起使用。

          使用方法:

          • 在用于运行 ef 工具的项目中,创建实现 IDesignTimeDbContextFactory&lt;YourContext&gt; 的类,
          • 实现方法YourContext CreateDbContext(string[] args),
          • args 将使用双破折号后传递给 ef 工具命令的命令行 args 填充(例如 dotnet ef migration add -- this will be passed
          • EF 只需要该方法返回上下文即可使用 - 因此您可以随意使用参数(需要连接字符串作为参数、配置路径等)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-07-28
            • 2018-01-05
            • 1970-01-01
            • 2014-06-30
            • 2011-11-24
            • 1970-01-01
            相关资源
            最近更新 更多