220

When I try to run my app I get the error

InvalidOperationException: Cannot resolve 'API.Domain.Data.Repositories.IEmailRepository' from root provider because it requires scoped service 'API.Domain.Data.EmailRouterContext'.

What's odd is that this EmailRepository and interface is set up exactly the same as far as I can tell as all of my other repositories yet no error is thrown for them. The error only occurs if I try to use the app.UseEmailingExceptionHandling(); line. Here's some of my Startup.cs file.

public class Startup
{
    public IConfiguration Configuration { get; protected set; }
    private APIEnvironment _environment { get; set; }

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        Configuration = configuration;

        _environment = APIEnvironment.Development;
        if (env.IsProduction()) _environment = APIEnvironment.Production;
        if (env.IsStaging()) _environment = APIEnvironment.Staging;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        var dataConnect = new DataConnect(_environment);

        services.AddDbContext<GeneralInfoContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.GeneralInfo)));
        services.AddDbContext<EmailRouterContext>(opt => opt.UseSqlServer(dataConnect.GetConnectString(Database.EmailRouter)));

        services.AddWebEncoders();
        services.AddMvc();

        services.AddScoped<IGenInfoNoteRepository, GenInfoNoteRepository>();
        services.AddScoped<IEventLogRepository, EventLogRepository>();
        services.AddScoped<IStateRepository, StateRepository>();
        services.AddScoped<IEmailRepository, EmailRepository>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();

        app.UseAuthentication();

        app.UseStatusCodePages();
        app.UseEmailingExceptionHandling();

        app.UseMvcWithDefaultRoute();
    }
}

Here is the EmailRepository

public interface IEmailRepository
{
    void SendEmail(Email email);
}

public class EmailRepository : IEmailRepository, IDisposable
{
    private bool disposed;
    private readonly EmailRouterContext edc;

    public EmailRepository(EmailRouterContext emailRouterContext)
    {
        edc = emailRouterContext;
    }

    public void SendEmail(Email email)
    {
        edc.EmailMessages.Add(new EmailMessages
        {
            DateAdded = DateTime.Now,
            FromAddress = email.FromAddress,
            MailFormat = email.Format,
            MessageBody = email.Body,
            SubjectLine = email.Subject,
            ToAddress = email.ToAddress
        });
        edc.SaveChanges();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
                edc.Dispose();
            disposed = true;
        }
    }
}

And finally the exception handling middleware

public class ExceptionHandlingMiddleware
{
    private const string ErrorEmailAddress = "errors@ourdomain.com";
    private readonly IEmailRepository _emailRepository;

    private readonly RequestDelegate _next;

    public ExceptionHandlingMiddleware(RequestDelegate next, IEmailRepository emailRepository)
    {
        _next = next;
        _emailRepository = emailRepository;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex, _emailRepository);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception,
        IEmailRepository emailRepository)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        var email = new Email
        {
            Body = exception.Message,
            FromAddress = ErrorEmailAddress,
            Subject = "API Error",
            ToAddress = ErrorEmailAddress
        };

        emailRepository.SendEmail(email);

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int) code;
        return context.Response.WriteAsync("An error occured.");
    }
}

public static class AppErrorHandlingExtensions
{
    public static IApplicationBuilder UseEmailingExceptionHandling(this IApplicationBuilder app)
    {
        if (app == null)
            throw new ArgumentNullException(nameof(app));
        return app.UseMiddleware<ExceptionHandlingMiddleware>();
    }
}

Update: I found this link https://github.com/aspnet/DependencyInjection/issues/578 which led me to change my Program.cs file's BuildWebHost method from this

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .Build();
}

to this

public static IWebHost BuildWebHost(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseDefaultServiceProvider(options =>
            options.ValidateScopes = false)
        .Build();
}

I don't know what exactly is going on but it seems to work now.

user1336
  • 6,435
  • 2
  • 27
  • 34
geoff swartz
  • 5,437
  • 11
  • 51
  • 75
  • 4
    What's happening there, is that the scope nesting isn't being validated; as in, it isn't checking, during runtime, if you have improper nesting of scope level. Apparently, this was turned off by default in 1.1. Once 2.0 came along, they turned it on by default. – Robert Burke May 20 '18 at 21:21
  • To anyone attempting to turn off the ValidateScopes, please read this https://stackoverflow.com/a/50198738/1027250 – Yorro Jan 17 '20 at 07:08

5 Answers5

398

You registered the IEmailRepository as a scoped service, in the Startup class. This means that you can not inject it as a constructor parameter in Middleware because only Singleton services can be resolved by constructor injection in Middleware. You should move the dependency to the Invoke method like this:

public ExceptionHandlingMiddleware(RequestDelegate next)
{
    _next = next;
}

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{
    try
    {
        await _next.Invoke(context);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex, emailRepository);
    }
}
TanvirArjel
  • 30,049
  • 14
  • 78
  • 114
user1336
  • 6,435
  • 2
  • 27
  • 34
  • 41
    Wow! Never knew you could inject into methods, is this just for middleware or can I use this trick in my own methods? – Fergal Moran Aug 09 '18 at 22:24
  • What about IMiddleware that's registered as scoped? I know for sure I get a new instance of middleware, but I still cant inject a scoped service to it. – macwier Oct 04 '18 at 11:43
  • 15
    @FergalMoran Unfortunately, this "trick" is a special behavior of just the middleware's `Invoke` method. However, you can achieve something similar via the autofac IoC lib and property injection. See [ASP.NET Core MVC Dependency Injection via property or setter method?](https://stackoverflow.com/questions/49112244/asp-net-core-mvc-dependency-injection-via-property-or-setter-method). – Felix K. Jan 01 '19 at 16:27
  • 9
    Injection is not magic. There is an engine behind the scenes that actually calls the dependency container to generate instances to pass as parameters to either constructors or methods. This particual engine looks for methods named "Invoke" with a first argument of HttpContext and then creates instances for the rest of the parameters. – Thanasis Ioannidis Oct 25 '19 at 21:17
  • 5
    "only Singleton services can be resolved by constructor injection in Middleware". Just learned a new thing! (and solved my problem :-) – Michel Oct 29 '21 at 13:22
  • Funny that I was using a middle ware injecting UserManager via the constructor, only in Development environment it crashed on startup, other envirornments not. Thanks! – CularBytes Aug 18 '22 at 20:49
213

Another way to get the instance of scoped dependency is to inject service provider (IServiceProvider) into the middleware constructor, create scope in Invoke method and then get the required service from the scope:

using (var scope = _serviceProvider.CreateScope()) {
    var _emailRepository = scope.ServiceProvider.GetRequiredService<IEmailRepository>();

    //do your stuff....
}

Check out Resolving Services in a Method Body in asp.net core dependency injection best practices tips tricks for more details.

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
Riddik
  • 2,565
  • 1
  • 11
  • 21
  • 8
    Super helpful, thanks! For anybody trying to access EF Contexts in middleware this is the way to go as they are scoped by default. – ntziolis Oct 16 '19 at 06:53
  • https://stackoverflow.com/a/49886317/502537 does this more directly – RickAndMSFT Nov 22 '19 at 03:08
  • 3
    At first I didn't think this worked, but then I realized you're doing `scope.ServiceProvider` instead of `_serviceProvider` on the second line. Thanks for this. – adam0101 Dec 24 '19 at 06:13
  • _serviceProvider.CreateScope().ServiceProvider does better for me – XLR8 Jul 26 '20 at 18:07
  • I think it would be best to use `IServiceScopeFactory` for this purpose – Francesco D.M. Nov 09 '20 at 12:51
  • 2
    Don't forget `using Microsoft.Extensions.DependencyInjection;` if you want to use `ServiceProvider.CreateScope()`. See here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions.createscope?view=dotnet-plat-ext-5.0 – Mark Cooper Dec 09 '20 at 13:27
  • 1
    When using a scoped service in an asp.net filter, and then trying to get the same scoped service in a singleton service method, the scope provided by the method shown here is not the same one that was used in the filter. – Richard Collette Mar 04 '21 at 18:59
  • Thanks! Helped me when trying to resolve handlers for AzureServiceBus events. – Árthur Mar 04 '22 at 20:48
  • It is good that this is here, but this should be avoided anywhere that it isn't absolutely a must. Keep your services clean and ignorant of the service provider if at all possible. – Don Rolling Aug 30 '22 at 19:15
44

Middleware is always a singleton so you can't have scoped dependencies as constructor dependencies in the constructor of your middleware.

Middleware supports method injection on the Invoke method,so you can just add the IEmailRepository emailRepository as a parameter to that method and it will be injected there and will be fine as scoped.

public async Task Invoke(HttpContext context, IEmailRepository emailRepository)
{

    ....
}
Joe Audette
  • 35,330
  • 11
  • 106
  • 99
  • I was in similar situation, then I added service using AddTransient, and it was able to resolve the dependency. I thought it wouldn't work since the middleware is singleton? bit strange.. – Sateesh Pagolu Jun 03 '18 at 13:24
  • 1
    I think a Transient dependency would have to be disposed manually, unlike scoped which will be automatically disposed at the end of the web request where it is first created. Maybe a transient disposable inside a scoped dependency would get disposed the the outer object is disposed. Still I'm not sure a transient dependency inside a singleton or an object with a longer than transient lifetime is a good idea, think I would avoid that. – Joe Audette Jun 03 '18 at 13:42
  • 2
    Even though you can inject a Transient-scoped dependency via the constructor in this case, it will not get instantiated as you would think. It will only happen once, when the Singleton is built. – Jonathan Oct 02 '18 at 21:01
  • 3
    You have mentioned that middleware is always a singleton, but it is not true. It is possible to create a middleware as a factory-based middleware and use it as a scoped-middleware. – Harun Diluka Heshan May 29 '19 at 11:57
  • 1
    Looks like factory based middleware was introduced in asp.netcore 2.2 and [documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility?view=aspnetcore-2.2) was created in 2019. So my answer was true when I posted it as far as I know. Factory based middleware does look like a good solution today. – Joe Audette May 29 '19 at 12:15
10

Your middleware and the service has to be compatible with each other in order to inject the service via the constructor of your middleware. Here, your middleware has been created as a convention-based middleware which means it acts as a singleton service and you have created your service as scoped-service. So, you cannot inject a scoped-service into the constructor of a singleton-service because it forces the scoped-service to act as a singleton one. However, here are your options.

  1. Inject your service as a parameter to the InvokeAsync method.
  2. Make your service a singleton one, if possible.
  3. Transform your middleware to a factory-based one.

A Factory-based middleware is able to act as a scoped-service. So, you can inject another scoped-service via the constructor of that middleware. Below, I have shown you how to create a factory-based middleware.

This is only for demonstration. So, I have removed all the other code.

public class Startup
{
    public Startup()
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<TestMiddleware>();
        services.AddScoped<TestService>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<TestMiddleware>();
    }
}

The TestMiddleware:

public class TestMiddleware : IMiddleware
{
    public TestMiddleware(TestService testService)
    {
    }

    public Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        return next.Invoke(context);
    }
}

The TestService:

public class TestService
{
}
Harun Diluka Heshan
  • 1,155
  • 2
  • 18
  • 30
7

In .NET Core 6, the below settings worked for me.

using (var scope = app.Services.CreateScope())
 {
     var services = scope.ServiceProvider.GetRequiredService<IDbInitilizer>;
     services.Invoke().Initialize();
 }

DBInitilizer

Md Mahmudul Hasan
  • 314
  • 1
  • 3
  • 13
  • Just for the record I'm also using .NET 6 and the accepted answer worked great for me without having to manually create a service scope. But maybe it doesn't work the same in this specific context (within the program.cs itself). – k3davis Jul 26 '23 at 23:28