Marcel
Marcel

Reputation: 1074

Add-Migration without parameterless DbContext and DbContextFactory constructor

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

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:

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.

Upvotes: 10

Views: 6601

Answers (5)

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)

Upvotes: 1

0xced
0xced

Reputation: 26508

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.

Upvotes: 1

npo
npo

Reputation: 1060

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

Upvotes: 1

0xced
0xced

Reputation: 26508

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

Upvotes: 4

Marcel
Marcel

Reputation: 1074

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

Upvotes: 2

Related Questions