10

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.

Community
  • 1
  • 1
Marcel
  • 1,002
  • 2
  • 16
  • 37
  • You might want to consider a facade with constructor injection (aka default values supplied by a static Service Locator): http://blog.ploeh.dk/2014/05/19/di-friendly-library/ –  Feb 17 '17 at 11:01
  • @AndreasNiedermair Thanks for the comment. But as far as I understand it needs a parameterless constructor. That's not what I want. I want to get `Add-Migration` or at least `DbMigrator.GetPendingMigrations()` running without a parameterless constructor. Because there are no default values I can provide. – Marcel Feb 17 '17 at 11:22
  • you have to provide some value for the parameter at some point - that's going to be the default value, resolved by a static Service Locator, which needs a specific injection/registration. anyway, this solution is more a workaround than a *good* solution ... :) –  Feb 17 '17 at 11:58
  • Possible duplicate of [How do I inject a connection string into an instance of IDbContextFactory?](http://stackoverflow.com/questions/18734496/how-do-i-inject-a-connection-string-into-an-instance-of-idbcontextfactoryt) –  Feb 17 '17 at 12:02
  • @AndreasNiedermair No. The marked solution implements `IDbContextFactory` without a constructor, so the default (parameterless) constructor is still in place. – Marcel Feb 17 '17 at 12:12
  • Actually, yes: it circumnavigates the exact same issue you are facing by utilizing `Database.SetInitializer<>` and shifting the dependency on the connection solely to the context instead additionally to the factory. And yes, it involves creating a custom `IDatabaseInitializer`. –  Feb 17 '17 at 12:22
  • @AndreasNiedermair 1) The non-marked solution mentioning the `Database.SetInitializer<>` doesn't work for calling the `Add-Migration` script. 2) I willingly would like to call `Database.SetInitializer(new MigrateDatabaseToLatestVersion(true));` and `MyContext.Database.Initialize(false);`. But I also would like to check `DbMigrator.GetPendingMigrations().Any()` instead running database initialization everytime. And `DbMigrator` doesn't have a `useSuppliedContext` switch. 3) Why must I create my own `IDatabaseInitializer`? – Marcel Feb 17 '17 at 14:03

5 Answers5

4

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

0xced
  • 25,219
  • 10
  • 103
  • 255
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.)

Marcel
  • 1,002
  • 2
  • 16
  • 37
1

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.

0xced
  • 25,219
  • 10
  • 103
  • 255
1

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

npo
  • 1,060
  • 8
  • 9
1

To extend answer https://stackoverflow.com/a/53778826/9941549 : the feature has been implemented finally, works ok with EfCore.Design 5.x package.

To use:

  • in project used to run ef tools against, create class implementing IDesignTimeDbContextFactory<YourContext>,
  • implement method YourContext CreateDbContext(string[] args),
  • args will be populated with command-line args passed to ef tools commands after double dash (e.g. dotnet ef migration add -- this will be passed)
  • EF requires only that method return context ready to use - thus you're free to use params as you want (require connection string as a parameter, path to config, anything)