Cannot resolve scoped service from root provider .Net Core 2
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 = "[email protected]";
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.
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);
}
}
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.
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)
{
....
}
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.
- Inject your service as a parameter to the
InvokeAsync
method. - Make your service a singleton one, if possible.
- Transform your
middleware
to afactory-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
{
}