本文介绍了没有无参数DbContext和DbContextFactory构造函数的Add-Migration的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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

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 的默认构造函数)而在生产环境中崩溃.我想在

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

  • 调试/发布(并且仅注入其他 IConnectionStringProvider )
  • 调用添加迁移脚本
  • 运行 DbMigrator.GetPendingMigrations()

目前,我收到了一些消息:

Currently I get some of those messages:

---更新---

这可能是我如何将连接字符串注入IDbContextFactory< T>的实例?没有解决方案.我解释了原因:

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:

  • 我总是将 Add-Migration 与连接字符串一起使用,那么如何提供使用它的 DbContext IDbContextFactory<> ?而不是无参数的构造函数?
  • I always use Add-Migration with connection string, so how can I provide a DbContext or IDbContextFactory<> that consumes it? Instead of parameterless constructors?

  • 这里有同样的问题:我使用 DbMigrator.GetPendingMigrations(),它也要求无参数的 DbContext IDbContextFactory<> 实现./li>

  • The same problem here: I use DbMigrator.GetPendingMigrations() which also asks for parameterless DbContext or IDbContextFactory<> implementations.
  • 据我了解,EntityFramework违反了封装暗示默认构造函数,并引起时间耦合,它不是故障安全的.因此,请提出一种无参数构造函数的解决方案.

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

    推荐答案

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

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

    当您运行 Add-Migration (没有默认构造函数)时,会发生以下情况:

    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()
    

    让我们看一下 DbMigrator 构造函数.从 Add-Migration 命令运行时, usersContext 为null, configuration.TargetDatabase null,并包含信息从命令行参数(例如 -ConnectionStringName -ConnectionString -ConnectionProviderName )传递.因此,将调用 new DbContextInfo(configuration.ContextType,configuration.TargetDatabase).

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

    为了不抛出 DbMigrator DbContextInfo 实例必须是可构造的.现在,让我们看一下 DbContextInfo 构造函数.为了使 DbContextInfo 具有可构造性, CreateActivator() CreateInstance()都不能返回null.

    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 基本上搜索DbContext类型或 IDbContextFactory< MyContext> 实现的无参数构造函数,并返回 Func< MyContext> .然后 CreateInstance 调用该激活器.不幸的是,激活器没有使用 DbContextInfo 构造函数的 DbConnectionInfo connectionInfo 参数,而是仅在创建上下文实例之后才应用(为简便起见,删除了无关的代码):

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

    然后,在 ApplyContextInfo 内部,魔术发生了:连接信息(来自 _connectionInfo )在新创建的上下文中被覆盖.

    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. 仅在 Debug 配置中进行编译时才添加默认构造函数.
    2. 如果未从 Add-Migration 命令调用,则默认构造函数将抛出该异常.
    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.

    这是我的上下文:

    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 ,如如何将连接字符串注入IDbContextFactory的实例中?.

    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? .

    这篇关于没有无参数DbContext和DbContextFactory构造函数的Add-Migration的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

    08-20 20:21