Why can't this particular configuration be resolved via dependency injection? - c#

I have a .NET Core 6 API project that already successfully loads configurations from appsettings.json into classes and also loads them into my services via dependency injection.
Now I wanted to add another configuration, but I just get the following exception when I call the service (or in this case Healthcheck - type names have been changed):
Unable to resolve service for type 'BrokenConfigClass' while attempting to activate 'HealthCheckUsingBrokenConfig'.
Stacktrace:
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetServiceOrCreateInstance[T](IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.HealthChecksBuilderAddCheckExtensions.<>c__3`1.<AddCheck>b__3_0(IServiceProvider s)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(HealthCheckRegistration registration, CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
[03:31:13 ERR] Connection id "0HMNND6U6RR42", Request id "0HMNND6U6RR42:00000001": An unhandled exception was thrown by the application.
However, my other configurations work and are set up exactly like the new one.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddConfig(configuration);
...
services
.AddHealthChecks()
.AddCheck<HealthCheckUsingBrokenConfig>(nameof(HealthCheckUsingBrokenConfig));
...
}
AddConfig is an extension:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddConfig(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<WorkingConfig1>(configuration.GetSection("Level1:SubLevel1:WorkingConfig1"));
services.Configure<WorkingConfig2>(configuration.GetSection("Level1:SubLevel2:WorkingConfig2"));
services.Configure<WorkingConfig3>(configuration.GetSection("Level1:SubLevel3:WorkingConfig3"));
services.Configure<BrokenConfigClass>(configuration.GetSection("HealthChecks:BrokenConfigClass"));
return services;
}
}
appsettings.json
{
"Level1": {
"SubLevel1": {
"WorkingConfig1": {
"Property1": "Value",
"Property2": "Value",
"Property3": "Value"
}
},
"SubLevel2": {
"WorkingConfig2": {
"Property1": "Value",
"Property2": "Value",
"Property3": "Value"
}
},
"SubLevel3": {
"WorkingConfig3": {
"Property1": "Value",
"Property2": "Value",
"Property3": "Value"
}
}
},
"HealthChecks": {
"BrokenConfigClass": {
"DelayInMinutes": 60
}
}
}
BrokenConfigClass:
public class BrokenConfigClass
{
public int DelayInMinutes { get; set; }
}
HealthCheckUsingBrokenConfig:
public class HealthCheckUsingBrokenConfig : IHealthCheck
{
private readonly BrokenConfigClass healthCheckConfig;
private readonly DatabaseContext databaseContext;
public HealthCheckUsingBrokenConfig(DatabaseContext databaseContext, BrokenConfigClass healthCheckConfig)
{
this.databaseContext = databaseContext;
this.healthCheckConfig = healthCheckConfig;
}
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// Some logic
}
}
When I call the healthcheck, I get the exception from above. When I try to load the config in a service, I get the same exception.
But when I load the other configs, everything works fine.
I double and triple checked the path to the section in appsettings.json. I copied the keys and pasted them into the GetSection() call.
When I call the following code, I also get the value 60 (from the appsettings.json):
configuration.GetSection("HealthChecks:BrokenConfigClass").GetValue<int>("DelayInMinutes")
Does anyone see my gross error or can explain what the problem is?
The configurations used here (like WorkingConfig1) have been renamed, but from the structure it should match. If an error should have sneaked in here (as for example in the paths), these should not be considered, since this is definitely not the case in the real code!

The exception appears accurate as nowhere do you explicitly add BrokenConfigClass to the container.
When calling Configure<BrokenConfigClass> you are actually adding IOptions<BrokenConfigClass>.
So either update the target class constructor according
//ctor
public HealthCheckUsingBrokenConfig(DatabaseContext databaseContext, IOptions<BrokenConfigClass> healthCheckConfig)
{
this.databaseContext = databaseContext;
this.healthCheckConfig = healthCheckConfig.Value;
}
or explicitly add the type to the container so that it can be resolved and injected
//...
services.Configure<BrokenConfigClass>(configuration.GetSection("HealthChecks:BrokenConfigClass"));
services.AddScoped<BrokenConfigClass>(sp => sp.GetRequiredService<IOptions<BrokenConfigClass>>().Value);

Related

How can I provide overridable configuration per endpoint in an AspNetCore web api?

I have an Asp.Net Core api middleware for which I want to expose some configuration options via appsettings.json:
public class MyMiddleware: IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// something like this, except get data from appsettings.json instead of attributes
var attribute = context.GetEndpoint()?.Metadata.GetMetadata<MyAttribute>();
if (attribute is not null) {
// do the things
}
await next(context);
}
}
The attribute pattern that I have has advantages that I would like to keep in that I can place the attribute on the action method, the class containing it or a base class and the middleware utilizes the most specific one. Unfortunately the attributes can only be modified by rebuilding the application and deploying a new version.
Is there a built-in way or otherwise reasonable way to map an options data store to endpoints?
I was thinking something in appsettings.json like this with globbing rules for path traversal similarities:
{
"ConfigurationRules": [
{
"Match": "/foo/**/bar",
"Option1": "..."
},
{
"Match": "/foo",
"Option1": "..."
},
{
"Match": "/**",
"Option1": "..."
},
]
}
This article explains how to map a custom configuration section to an object that you can use in code. Your middleware would have to accept an IConfiguration object so that the caller can pass it into your setup method inside of ConfigureServices. Something like this:
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
//...
public void ConfigureServices(IServiceCollection services)
{
services.AddMyMiddleware(Configuration);
}
Then in your AddMyMiddleware extension method, you can get your configuration section like this:
var rules = Configuration.GetSection("ConfigurationRules");
services.Configure<MyConfigurationRulesClass>(rules);
Now this instance of MyConfigurationRulesClass will be available through dependency injection wherever you need it - e.g. by adding IOptions<MyConfigurationRulesClass> myConfiguration to the constructor

Inject instance of ILogger in my component class Azure Functions using Autofac

I am writing a simple Azure function.
I have installed the AzureFunctions.Autofac nuget package, and would like to use this as my DI library.
I have set up the following AutofacConfig class to register my types:
public class AutofacConfig
{
public AutofacConfig(string functionName)
{
DependencyInjection.Initialize(builder =>
{
//do all of you initialization here
//db client
builder.RegisterType<EventComponent>()
.As<IComponent<EventModel>>().SingleInstance();
}, functionName);
}
}
Here is my EventComponent class, to which I would like to inject the ILogger instance provided.
public class EventComponent : IComponent<EventModel>
{
private ILogger _log;
public EventComponent(ILogger logger)
{
_log = logger;
}
}
Here is how I inject my EventComponent:
[FunctionName("AddEvent")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, ILogger log, [Inject]IComponent<EventModel> component)
{
log.LogInformation("C# HTTP trigger function processed a request.");
await component.Add(new EventModel() { Id = Guid.NewGuid(), Description = $"Test description nr: {new Random().Next(1, 100000)}", User = "Test User" });
return req.CreateResponse(HttpStatusCode.OK);
}
The problem is, I get an exception on the above, because Autofac cannot resolve the parameter Microsoft.Extensions.Logging.ILogger.
Here is the exception message:
Exception binding parameter 'component'... Cannot resolve parameter 'Microsoft.Extensions.Logging.ILogger logger' of constructor 'Void .ctor(Microsoft.Extensions.Logging.ILogger)'. (See inner exception for details.) -> None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'Event.Function.Components.EventComponent' can be invoked with the available services and parameters:\r\nCannot resolve parameter 'Microsoft.Extensions.Logging.ILogger logger' of constructor 'Void .ctor(Microsoft.Extensions.Logging.ILogger)'.",
How can I inject the ILogger instance into my EventComponent class?
In Azure Functions V2, the ILogger is injected by default. Also, here are two very nice articles on dependency inject in Azure Functions.
https://blog.mexia.com.au/dependency-injections-on-azure-functions-v2
and http://codingsoul.de/2018/01/19/azure-function-dependency-injection-with-autofac/
I found your question when looking for the same thing. Have you found a solution?
Because I don't think that is possible. ILogger log is injected by the framework and I don't see how it could be referenced from your AutofacConfig-class.
How I resolved this was by changing the EventComponent-class to use Setter-injection instead of Constructor-injection, like this:
public class EventComponent : IComponent<EventModel>
{
public ILogger Log { get; set; }
}
and change your function to set the Log-property:
[FunctionName("AddEvent")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, ILogger log, [Inject]IComponent<EventModel> component)
{
log.LogInformation("C# HTTP trigger function processed a request.");
component.Log = log;
await component.Add(new EventModel() { Id = Guid.NewGuid(), Description = $"Test description nr: {new Random().Next(1, 100000)}", User = "Test User" });
return req.CreateResponse(HttpStatusCode.OK);
}
The downside is that you need to remember to set that value at the start of every function that uses that class, but the injection works.
If you want to inject the ILogger into a function app you need to do the following:
Add the correct log level and namespace to your host.json
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true
}
},
"logLevel": {
"YourNameSpace": "Information"
}
Inject ILogger<T> where T is your function app class name/type. In this sample my function app class name is Api.
public class TestService : ITestService
{
private readonly ILogger<Api> _logger;
public TestService(ILogger<Api> logger)
{
_logger = logger;
}
public void LogSomething(string message)
{
_logger.LogInformation(message);
}
}

serilog format SourceContext for showing only assembly name

I configured my project to use Serilog for logging using dependecy injection.
I use the following schema in the classes constructor:
namespace FlickPopper.API.Controllers {
public class ValuesController : Controller {
private ILogger<ValuesController> _logger;
public MyClass(ILogger<ValuesController> logger) {
_logger = logger;
}
}
}
In this way, serilog creates the logger calling to Log.ForContext<classname>
My settings are:
"Serilog": {
"WriteTo": [
{
"Name": "RollingFile",
"Args": {
"pathFormat": "Logs\\log-{Date}.txt",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}"
}
}
],
"Enrich": [ "FromLogContext" ]
}
So, the logs seems like this:
2018-01-26 22:20:08 [INF] [FlickPopper.API.Controllers.ValuesController] Get all values
It's any way to format the SourceContext property to show only the assembly name in the logs, somthing like this?
2018-01-26 22:20:08 [INF] [FlickPopper.API] Get all values
When you use injected ILogger<T>, under the hood Serilog logger is created by SerilogLoggerProvider that implements ILoggerProvider interface. This interface has the only method:
public interface ILoggerProvider : IDisposable
{
ILogger CreateLogger(string categoryName);
}
The passed categoryName is used as value for {SourceContext} property in message format. And ASP.NET Core passes it as fully-qualified name (e.g. FlickPopper.API.Controllers.ValuesController).
So this string value should be fixed not in Serilog code or configuration, but in ASP.NET Core logging infrastructure.
The responsible class for creation of that value in first place is Logger<T> class. Instances of Logger<T> are instantiated when you inject ILogger<T> into your classes. Here is source code of its constructor:
public class Logger<T> : ILogger<T>
{
public Logger(ILoggerFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
_logger = factory.CreateLogger(TypeNameHelper.GetTypeDisplayName(typeof(T)));
}
// ...
}
That call to TypeNameHelper.GetTypeDisplayName(typeof(T)) returns fully-qualified name that then is passed to ILoggerFactory and eventually to SerilogLoggerProvider.
So if you want to change that behavior and adjust the categoryName passed to ILoggerFactory, you should have your own implementation of ILogger<T> that makes required call to ILoggerFactory.CreateLogger(). It's not that difficult, because Logger<T> class is very thin and is based on Microsoft.Extensions.Logging.Logger implementation. Here is the class that is a copy of Logger<T> except for one line producing logger category name:
public class LoggerEx<T> : ILogger<T>
{
private readonly ILogger _logger;
public LoggerEx(ILoggerFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
_logger = factory.CreateLogger(typeof(T).Assembly.GetName().Name);
}
IDisposable ILogger.BeginScope<TState>(TState state)
{
return _logger.BeginScope(state);
}
bool ILogger.IsEnabled(LogLevel logLevel)
{
return _logger.IsEnabled(logLevel);
}
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
You should also replace the standard implementation of ILogger<T> with this one in your services registration:
services.AddSingleton(typeof(ILogger<>), typeof(LoggerEx<>));
Now the instances of LoggerEx<T> will be injected to the controllers and {SourceContext} will have a value you built:
2018-01-27 09:54:21 [INF] [TestProject.TestApplication] Hello!
I had a similar problem, except I wanted to include only the class/interface name.
I solved this by creating a custom enricher.
// Create logger
var logger = new LoggerConfiguration()
// ...
.Enrich.With(new SourceContextEnricher())
// ...
.CreateLogger();
public class SourceContextEnricher : ILogEventEnricher {
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) {
if (logEvent.Properties.TryGetValue("SourceContext", out var property)) {
var scalarValue = property as ScalarValue;
var value = scalarValue?.Value as string;
if (value?.StartsWith("FlickPopper.API.") ?? false) {
var lastElement = value.Split(".").LastOrDefault();
if (!string.IsNullOrWhiteSpace(lastElement)) {
logEvent.AddOrUpdateProperty(new LogEventProperty("SourceContext", new ScalarValue(lastElement)));
}
}
}
}
}

Why does Scoped service resolve as two different instances for same request?

I have a simple service that contains a List<Foo>. In Startup.cs, I am using the services.addScoped<Foo, Foo>() method.
I am inject the service instance in two different places (controller and middleware), and for a single request, I would expect to get the same instance. However, this does not appear to be happening.
Even though I am adding a Foo to the List in the Controller Action, the Foo list in the Middleware is always empty. Why is this?
I have tried changing the service registration to a singleton, using AddSingleton() and it works as expected. However, this has to be scoped to the current request. Any help or ideas are greatly appreciated!
FooService.cs
public class FooService
{
public List<Foo> Foos = new List<Foo>();
}
Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<FooService, FooService>();
}
[Below are the two places where I am injecting the service, resulting in two different instances]
MyController.cs
public class MyController : Controller
{
public MyController(FooService fooService)
{
this.fooService = fooService;
}
[HttpPost]
public void TestAddFoo()
{
//add foo to List
this.fooService.Foos.Add(new Foo());
}
}
FooMiddleware.cs
public AppMessageMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
{
this.next = next;
this.serviceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
{
context.Response.OnStarting(() =>
{
var fooService = this.serviceProvider.GetService(typeof(FooService)) as FooService;
var fooCount = fooService.Foos.Count; // always equals zero
return Task.CompletedTask;
});
await this.next(context);
}
That's because when you inject IServiceProvider into your middleware - that's "global" provider, not request-scoped. There is no request when your middleware constructor is invoked (middleware is created once at startup), so it cannot be request-scoped container.
When request starts, new DI scope is created, and IServiceProvider related to this scope is used to resolve services, including injection of services into your controllers. So your controller resolves FooService from request scope (because injected to constructor), but your middleware resolves it from "parent" service provider (root scope), so it's different. One way to fix this is to use HttpContext.RequestServices:
public async Task Invoke(HttpContext context)
{
context.Response.OnStarting(() =>
{
var fooService = context.RequestServices.GetService(typeof(FooService)) as FooService;
var fooCount = fooService.Foos.Count; // always equals zero
return Task.CompletedTask;
});
await this.next(context);
}
But even better way is to inject it into Invoke method itself, then it will be request scoped:
public async Task Invoke(HttpContext context, FooService fooService)
{
context.Response.OnStarting(() =>
{
var fooCount = fooService.Foos.Count; // always equals zero
return Task.CompletedTask;
});
await this.next(context);
}
First of all you shouldn't be using GetService, use the proper DI system that is in place by passing it into the Invoke method as a parameter.
Secondly, the reason you are getting a different object is because the constructor of the middleware is called outside of the scope of any request, during the app initialisation phase. So the container used there is the global provider. See here for a good discussion.
public class AppMessageMiddleware
{
private readonly RequestDelegate _next;
public AppMessageMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
{
_next = next;
}
//Note the new parameter here: vvvvvvvvvvvvvvvvvvvvv
public async Task Invoke(HttpContext context, FooService fooService)
{
context.Response.OnStarting(() =>
{
var fooCount = fooService.Foos.Count;
return Task.CompletedTask;
});
await _next(context);
}
}

Custom NLog LayoutRenderer with constructor using Dependency Injection

I am trying to write a custom LayoutRenderer that logs data read from an object, but it seems that NLog is not working properly with Dependency Injection.
Here is my CustomLayoutRenderer:
[LayoutRenderer("custom-value")]
public class CustomLayoutRenderer : LayoutRenderer
{
private readonly RequestContext _context;
public CustomLayoutRenderer(RequestContext context)
{
_context = context;
}
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
builder.Append(_context.CustomValue);
}
}
It is using this RequestContext object:
public class RequestContext
{
public string CustomValue { get; set; } = "Valid custom value";
}
I am also wiring up DI, configuring NLog and registering my LayoutRenderer in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<RequestContext>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
LayoutRenderer.Register<CustomLayoutRenderer>("custom-value");
loggerFactory.AddNLog();
app.AddNLogWeb();
env.ConfigureNLog("nlog.config");
// ...
}
I am then trying to use my ${custom-value} in nlog.config, but I am getting an error on the AddNLog() call:
2017-02-03 13:08:08.0284 Error Parsing configuration from [project-folder]\bin\Debug\net452\win7-x64\NLog.config failed.
Exception: NLog.NLogConfigurationException: Exception when parsing [project-folder]\bin\Debug\net452\win7-x64\NLog.config.
NLog.NLogConfigurationException: Cannot access the constructor of type: ATest.CustomLayoutRenderer. Is the required permission granted?
at NLog.Internal.FactoryHelper.CreateInstance(Type t)
...
Notes
The reason why I am trying this is that I would like to log some information accessible only from the controller (like the TraceIdentifier, parts of the URL, and some request-specific custom stuff). The values in RequestContext would be set by the controller when it gets a request.
The following Renderer works as expected, which makes me think this is a dependency injection problem:
[LayoutRenderer("custom-value")]
public class CustomLayoutRenderer : LayoutRenderer
{
protected override void Append(StringBuilder builder, LogEventInfo logEvent)
{
builder.Append("Hello, World!");
}
}
I did see this NLog bug but it's marked as fixed now, which is why I am posting here rather than there.
And for the sake of completeness, here is what I have added to my project.json:
"dependencies": {
...
"NLog.Extensions.Logging": "1.0.0-*",
"NLog.Web.AspNetCore": "4.3.0"
},
Two methodes:
1) DI aware
You could make NLog DI aware. Add to your startup.cs:
ConfigurationItemFactory.Default.CreateInstance = (Type type) =>
{
// your custom target. Could be a better check ;)
if(type == typeof(CustomLayoutRenderer))
return new CustomLayoutRenderer(...); // TODO get RequestContext
else
return Activator.CreateInstance(type); //default
};
This is a more generic approach.
2) AspNetMvcLayoutRendererBase
Or, override from AspNetMvcLayoutRendererBase (NLog.Web.AspNetCore) and use HttpContextAccessor?.HttpContext?.TryGetRequest() and don't add the constructor.
This only works when needing HttpContext.
[LayoutRenderer("custom-value")]
public class MyCustomRenderer : AspNetLayoutRendererBase
{
protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
{
var httpRequest = HttpContextAccessor?.HttpContext?.TryGetRequest();
if (httpRequest == null)
return;
builder.Append(httpRequest.Something); //TODO
}
}
Looking at the NLog source code for FactoryHelper. It looks like you have to provide a default constructor.
You may want to also consider using the layout renders from NLog.Web package. There are several that can get request-specific information. There even an option to log stuff from HttpContext with ${aspnet-Item} layout
If that doesn't suit you perhaps a "stateful" logger will work like the one in the answer I gave here: https://stackoverflow.com/a/32857908/1289052

Categories

Resources