dotnet/efcore

DbContextFactory could be more tolerant of there being multiple constructors on a DbContext

lowds opened this issue · 6 comments

lowds commented

When creating a DbContext we often have one constructor that accepts DbContextOptions and another no-arg constructor so the DbContext will work with the EF tooling for creating migrations. However, when using DbContextFactory having more than one constructor on the DbContext results in the factory being unable to create a new DbContext and results in an exception

System.InvalidOperationException
Multiple constructors accepting all given argument types have been found in type

This is due to the implementation of DbContextFactorySource that mandates a single public non-static constructor

if (constructors.Length == 1)

Although there are work-arounds available with regards to the EF core tooling (https://docs.microsoft.com/en-gb/ef/core/cli/dbcontext-creation?tabs=dotnet-core-cli) it would be great if these were not required just because we are using DbContextFactory.


The DbContextFactorySource could be changed so the first 'if' statement is replaced with a for-each loop, iterating over each candidate constructor. The rest of the code inside the existing if statement is already checking parameter count and parameter types, so the for-each change would still allow the DbContextFactorySource to find the constructor that accepted DbContextOptions, but ignore any other constructors that may be present on the DbContext

@lowds The if line that you reference is an optimization for a specific case where there is one parameter and it is the DbContextOptions. The general case is ActivatorUtilities.CreateFactory(typeof(TContext), new Type[0]);, which uses the D.I. system to resolve dependencies. The D.I. system has limitations with multiple constructors--see https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#constructor-injection-behavior. Issues in the D.I. system should be filed at https://github.com/dotnet/runtime

lowds commented

@ajcvickers thanks for the response. I think we've just run into this situation because are migrating from using DI with DbContext, to using DbContextFactory as we bring server-side blazor into our solution.

We have a stand-alone console application acting as our startup project (referencing EF design and tools packages) for creating EF migrations where rather than a web app where the DI services are configured (as our DbContext is in a .NET standard library) so the no-arg constructor was being used to support this.

Our DbContext currently has two constructors:

  • no-arg to support EF tooling when creating migrations
  • DbContextOptions so that configuration happens via the extension method services.AddDbContextFactory(options)

With the two constructors everything used to work when registering a DbContext via services, but moving to DbContextFactory to support Blazor has meant that EF now expects our DbContext to have exactly one constructor. I made a small XUnit file showing this at https://gist.github.com/lowds/d029bb8eefda07d0e18a04a7f7c42daa .

Currently we're working around it by registering our own CustomDbContextFactorySource in the services collection for our web app, where the custom source contains the for-loop proposed in my previous comment, though will admit this is not ideal as the IDbContextFactorySource is documented as an internal API.

If when using DbContextFactory our DbContext must have exactly one constructor then long term we will look to change our migrations startup project to use one of the workarounds specific to the tooling (i.e. implement design-time factories).

Note for triage: Related, possibly same root cause: dotnet/runtime#45119

Note for triage: not a duplicate of dotnet/runtime#45119. In this case, the exception is correct because both constructors are valid. However, we could in EF Core select the single parameter constructor here instead of falling back to D.I. This wouldn't fix the general case, but would make the case with these two common constructors work.

Currently we're working around it by registering our own CustomDbContextFactorySource in the services collection for our web app, where the custom source contains the for-loop proposed in my previous comment, though will admit this is not ideal as the IDbContextFactorySource is documented as an internal API.

Do you have an example how you worked around this? I'm having the same issue and I can't seem to figure this out.

Another way to workaround it is to combine 2 similar constructors into one and allow null parameters like this:

    public ProductDbContext(DbContextOptions<ProductDbContext> options, IAuditUser auditUser = null) : base(options)
    {
        _userName = (auditUser == null) ? "System" : auditUser.Name;
    }