I have a .NET Core 2.2 WebAPI project in which I'm registering three services (we'll call them MailerService
, TicketService
, and AuditServce
), plus a middleware (ExceptionMiddleware
) that depends on one of those services (MailerService
). MailerService
and TicketService
both depend on strongly-typed options objects, which I register with service.Configure<TOption>()
. I've made sure that the options objects are registered before the services, and the options dependencies themselves are wired into the services' constructors.
The issue is that TicketService
resolves its options object just fine from DI, but for some reason the config for MailerService
resolves AFTER the service itself. Rough sketch of relevant code below.
I've set breakpoints to watch the order of resolution, and the delegate for setting MailerConfig consistently fires AFTER the MailerService constructor. So every time I get an instance of MailerSerivce, its options parameter is NULL. And yet, watching the same resolution for TicketService, TicketConfig resolves before the TicketService constructor fires, and TicketService gets a properly-configured options object. Aside from MailerService being a dependency of the middleware, I can't figure out what might be different between them.
I've been banging my head on this for hours now, but can't find any decent documentation explaining why the DI resolution order might get out of whack, or what I might have done wrong here. Does anyone have a guess at what I might be doing wrong? Does the exception middleware need to be registered as a service as well?
Startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters()
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
services.Configure<MailerConfig>(myOpts =>
{
// this always resolves AFTER MailerService's constructor
myOpts = Configuration.GetSection("MailerSettings").Get<MailerConfig>();
myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("MailerApiKeyFile"));
});
services.Configure<ExceptionMiddlewareConfig>(myOpts =>
{
myOpts.AnonymousUserName = Configuration.GetValue<string>("AnonymousUserName");
myOpts.SendToEmailAddress = Configuration.GetValue<string>("ErrorEmailAddress");
});
services.Configure<TicketConfig>(myOpts =>
{
// this always resovles BEFORE TicketService's constructor
myOpts.ApiRoot = Configuration.GetValue<string>("TicketApiRoot");
myOpts.SecretKey = _GetApiKey(Configuration.GetValue<string>("TicketApiKeyFile"));
});
services.AddTransient(provider =>
{
return new AuditService
{
ConnectionString = Configuration.GetValue<string>("Auditing:ConnectionString")
};
});
services.AddTransient<ITicketService, TicketService>();
services.AddTransient<IMailerService, AuditedMailerService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<ExceptionMiddleware>();
//app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
}
MailerService Constructor
public AuditedMailerService(AuditService auditRepo, IOptions<MailerConfig> opts)
{
// always gets a NULL opts object??????
_secretKey = opts.Value.SecretKey;
_defaultFromAddr = opts.Value.DefaultFromAddress;
_defaultFromName = opts.Value.DefaultFromName;
_repo = auditRepo;
}
TicketService Constructor
public TicketService(IOptions<TicketConfig> opts)
{
// always gets an initialized opts object with proper values assigned
ApiRoot = opts.Value.ApiRoot;
SecretKey = opts.Value.SecretKey;
}
Middleware Constructor
public ExceptionMiddleware(RequestDelegate next, IMailerService mailer, IOptions<ExceptionMiddlewareConfig> config)
{
_mailer = mailer;
_next = next;
_anonymousUserName = config.Value.AnonymousUserName;
_sendToEmailAddress = config.Value.SendToEmailAddress;
}
While this isn't much of an answer (I still have no idea why DI was only resolving the options after the service), I have found a solution to the problem. I'm just making an end-run around the Options Pattern and resolving all dependencies explicitly inside the delegate where I register the mailer service. I also tweaked ExceptionMiddleware to take the mailer service as a method argument in InvokeAsync, rather than a constructor argument. It's not terribly important that the services be transient or singletons, but for the time being I just prefer transients.
The notable downside of this approach is that I can no longer use the live-update mechanisms built into the options system - if I change a value in my appsettings on the fly, the app will need to be recycled to pick it up. That isn't an actual need in my app, so I can live with it, but others should be mindful before following my approach.
New MailerService registration delegate: