0

I've hit a strange problem With EF Core and I can't understand why ...

public class Startup
{
    static Config Config;

    public Startup(IConfiguration configuration)
    {
        Config = new Config();
        configuration.Bind(Config);
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpContextAccessor();
        services.AddScoped(ctx => ctx.GetService<IHttpContextAccessor>()?.HttpContext);
        services.AddScoped(ctx => ctx.GetService<HttpContext>()?.Request);
        services.AddAuthInfo();
        services.AddSingleton(Config);

        services.AddDbContext<MembershipDataContext>(options => options.UseSqlServer(Config.Connections["Membership"]));
        services.AddDbContext<CoreDataContext>(options => options.UseSqlServer(Config.Connections["Core"]));
        services.AddDbContext<B2BDataContext>(options => options.UseSqlServer(Config.Connections["B2B"]));

        services.AddScoped<IMembershipDataContext, MembershipDataContext>();
        services.AddScoped<ICoreDataContext, CoreDataContext>();
        services.AddScoped<IB2BDataContext, B2BDataContext>();

        ...

... I extract custom auth information from each request and inject that in to my DbContexts.

Because of the init process I had to have a CTOR that accepts DbContextOptions so I simply added a second in the hope that it would call the right one ...

public EFDataContext(DbContextOptions options, IAuthInfo auth) : base(options) { AuthInfo = auth; }

public EFDataContext(DbContextOptions options) : base(options) { }

... At run time I see both CTOR's get hit several times inside a single request (not what I was expecting).

From other posts I note that many are saying I don't need the last three lines but removing them gives me exceptions telling me that other objects can no longer be constructed by DI.

So i'm confused ...

How do I get this working so that I can only construct 1 instance per request and only hit the CTOR with the most params when I do?

Steven
  • 166,672
  • 24
  • 332
  • 435
War
  • 8,539
  • 4
  • 46
  • 98
  • Why do you do `AddScoped` for your contexts? `AddDbContext` will add them already as scoped services. – DavidG Jul 11 '19 at 12:34
  • That's explained in the question – War Jul 11 '19 at 12:34
  • Hmm well I'm not sure you can extend DbContext constructors, I've not tried, but the reason I haven't tried is because I don't think you should be injecting auth info into it in the first place. – DavidG Jul 11 '19 at 12:40
  • It's a custom object defined to help me determine multi-tenancy context within the DB which holds transactional information, based on the context the filters are modified ... this works perfectly in EF6 ... doesn't matter though, the point is EF Contexts should be able to accept other args as part of the standard premise for DI in the first place. – War Jul 11 '19 at 12:45
  • @DavidG i spotted it ... :) – War Jul 11 '19 at 12:56

2 Answers2

0

So I figured out if I modify factory code I can handle construction fine during "setup" scenarios for the db.

That then allowed me to have only a single CTOR on the context which is the one I need, The net result though is that I still need those extra lines or it fails with a DI exception ...

Unable to resolve service for type 'IMyContext' while attempting to activate 'SomeOther type'.

Then it clicked ... I don't ever ask for a "MyDbContext" I always ask for a "IMyDbContext" ... the extra lines are to map the interface calls in the container to the strongly typed context not to "re-add the context" to the container.

War
  • 8,539
  • 4
  • 46
  • 98
0

You should not be adding your contexts separately as scoped. That's the main source of your problem. When you use AddDbContext, it registers your DbContext-derived class. If you go on to request an interface, it has no idea how to satisfy that interface; it only knows how to satisfy the class. That's why you get exceptions, and it's why you felt the need to add the separate interface registrations. However, when you do that, you're requesting a different instance of your context class as it's an entirely different registration.

You don't need an interface for your context. I see people do this all the time, and it's just mind-numbing. There is absolutely zero point. The only thing that should be publicly accessible on your context should be your DbSet properties and built-in methods like Add, SaveChangesAsync, etc. If you want or need to generically work with the DbSets, there's Set<T> for that, so your context class can just be injected directly. Besides, it's not like your going to have multiple implementations of this context. It's a representation of your database, and as such, it is what is: one implementation for all time.

Additionally, you should not be injecting anything into your context other than DbContextOptions. If you need a service, DbContext has an internal service provider, and that's what you should be using, i.e. this.GetService<IAuthInfo>.

UPDATE

Based on the comment thread, I don't think I've been clear about what I was suggesting. You should have something like:

public interface IService
{
    void DoSomething();
}

Then, you'd create implementations of this for each of your providers, e.g. CosmosDbService, RavenDbService, EFService, etc. Then in the EFService, you'd inject your context class, not an interface:

public class EFService : IService
{
    private readonly MyDbContext _context;

    public EFService(MyDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        // use _context
    }
}

In this way, the context is only there for the sake of EF. There's nothing extraneous added to it and no business logic exists there. Everywhere else in your code, you inject IService, and then you sub in the appropriate provider implementation (EF, Cosmos, Raven, etc.) via the DI container. All your business logic lives in the service class, as it should, and you don't need an interface for your context.

This also likely then means you also don't need extraneous services like your IAuthInfo in your context, either, meaning you can avoid the internal service provider. You'd instead inject IAuthInfo into your service class.

What you're doing now, is the exact opposite. You're fundamentally entangling your business logic with EF by making the context implement an interface that has nothing to do with its purpose: serving as a simple unit of work.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • In my stack I do need the interfaces ... It may not be implemented by EF and the EF code lives in a separate assembly to the business logic which is separated from say the Cosmos code or raven code which doesn't use EF. The interface is my abstraction of a data source which just "happens" to be fulfilled by EF in this context. – War Jul 11 '19 at 12:58
  • That last point about the internal service provider I didn't know though ... i'll tinker with that. – War Jul 11 '19 at 12:59
  • I get what your saying, but that's still the wrong approach. You should have a service that internally uses different data providers. You can have an interface *for that*, then. Using the context directly as the implementation is either going to result in 1) all your other providers having to conform to EF conventions or 2) making your context do more than it should. – Chris Pratt Jul 11 '19 at 13:01
  • My interface exposes IQueryable for gets and generic Add, Update, ect methods ... there's nothing specific to EF in it ... In my case due to "business reasons" we often find implementations vary and the stack pulls libs based on config ... it's hard to explain ... but imagine a code stack that builds itself as part of an init cycle from components it grabs from various http endpoints, the net result is an app. Having the DI container inside the context though ... that's anti-pattern straight up ... https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ – War Jul 11 '19 at 13:05
  • Another example from here: https://stackoverflow.com/questions/22795459/is-servicelocator-an-anti-pattern – War Jul 11 '19 at 13:06
  • That's what I'm talking about. The one implementation has to satisfy the needs of both EF (adding `DbSet` properties so entities can be tracked) *and* exposing `IQueryable` properties so that it can conform to the interface, which likely just proxy to the former. You're making the class do too much. As far as the internal service provider goes, it's somewhat similar to service-locator, which can be described as an "anti-pattern", but just because something is an anti-pattern doesn't mean there's no place for: just that people over-use it places it shouldn't be used. – Chris Pratt Jul 11 '19 at 13:10
  • In EF's case, it's necessary to allow interaction with services that might be required without having to expose them as constructor parameters which would be problematic for all the EF commands, which need to instantiate a context, and may not be able to provide said services. – Chris Pratt Jul 11 '19 at 13:11
  • I like many others don't like the idea of tightly coupling EF to business logic ... I can't understand your objection to that ... EF having concerns about tracking entities is EF's concern not my business logics. In all my DataContexts EF or otherwise I have the ability to perform actions that affect DB rows then call save changes to apply the total transaction to the entire stack ... my business logic knows only that the context can take part in such a transaction and wrapping the save changes call in a transaction scope is all it needs to care about. What's EF specific about that? – War Jul 11 '19 at 13:18
  • But that's *exactly* what you're doing, and it's the exact point I've been making. By making your context actually be the unit of this logic, rather than simply a dependency to the unit of the logic, you're entangling you business layer into EF. – Chris Pratt Jul 11 '19 at 14:53
  • See my update above. Hopefully, that will make things more clear. – Chris Pratt Jul 11 '19 at 15:16