How to inject settings from appsettings.json to an ILoggerProvider? - configuration

I'm implementing a custom ILogger<T> due to some special requirements, and one of those requirements is to have a section, inside the Logging section in the project's appsettings.json, with configuration values for that logger. Question is, what's the right way (or a good way) to inject those settings in the logger? I presume I should start by injecting those settings in the corresponding ILoggerProvider and have the provider instantiate a logger with those settings, but I'm stumped on how to properly inject those values in the provider.
So far I have this in Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((context, logging) =>
{
logging.ClearProviders();
logging.AddConfiguration(context.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddProvider(new CustomLoggerProvider());
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
, the relevant section in appsettings.json is:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"CustomLogger": {
"Level": "Error",
"Url": "http://[URL]"
}
},
, and the provider implementation is as follows:
[ProviderAlias("CustomLogger")]
public class CustomLoggerProvider : ILoggerProvider
{
private readonly Dictionary<string, CustomLogger> _loggers;
private readonly LogLevel _level;
private bool _already_disposed;
public LogLevel Level
{
get => _level;
}
public CustomLoggerProvider()
{
//Here's where I don't know what should I do to inject the proper config values
_level = LogLevel.Error;
}
public ILogger CreateLogger(string categoryName)
{
if (_loggers.ContainsKey(categoryName))
{
return _loggers[categoryName];
}
var l = new CustomLogger(this);
_loggers.Add(categoryName, l);
return l;
}
// Rest of implementation omitted for simplicity
}

I found an acceptable answer and I'll post it here, in case someone needs it.
Steps are:
Implement your ILoggerProvider and your ILogger. The examples on Microsoft's page are a good guide.
Have your ILoggerProvider implementation accept the needed settings on its constructor (via IOptions<T> or simple parameters; I'm using an options object called MyCustomLoggerSettings).
Register your ILoggerProvider in Startup.ConfigureServices() as a singleton, like this (let's say my implementation is called MyCustomLoggerProvider):
services.AddSingleton<ILoggerProvider>(p =>
{
var options = p.GetService<IOptions<MyCustomLoggerSettings>>();
return new MyCustomLoggerProvider(options);
});
Done.
As a bonus, in this way you can pass any other dependencies to this object (including objects registered on the service collection; the use of method overloads in services that accept an anonymous function give you the service collection as a parameter, and there you can use GetService<T> to resolve any dependencies.

Related

ASP.NET Core 5.0 application using React.js and MySQL as Database

Recently I am trying new ASP.NET Core 5.0 with React.js. I want to use MySql as my database server.
Now here I want to use ASP.NET Identity Server for Membership which we previously had in ASP.NET versions.
I tried to follow a tutorial listed over here: Video Link
Now in appsettings.json here is the code
{
"ConnectionStrings": {
"DefaultConnection": "server=localhost; port=3306; database=trom; user=root; password=hello#world; CharSet=utf-8"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"IdentityServer": {
"Clients": {
"Trom": {
"Profile": "IdentityServerSPA"
}
}
},
"AllowedHosts": "*"
}
Now in the Startup.cs I am trying to make changes as suggested in video
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Trom.Data;
using Trom.Models;
namespace Trom
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
}
The error I am getting is
There are many ways suggested on documentation website. But nothing seems to be working out. Is there any workaround for this?
The video is a bit older and uses a Pomelo version for EF Core 3.0.
For 5.0, take a look at the sample code that we show on our repository home page:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Replace with your connection string.
var connectionString = "server=localhost;user=root;password=1234;database=ef";
// Replace with your server version and type.
// Use 'MariaDbServerVersion' for MariaDB.
// Alternatively, use 'ServerVersion.AutoDetect(connectionString)'.
// For common usages, see pull request #1233.
var serverVersion = new MySqlServerVersion(new Version(8, 0, 26));
// Replace 'YourDbContext' with the name of your own DbContext derived class.
services.AddDbContext<YourDbContext>(
dbContextOptions => dbContextOptions
.UseMySql(connectionString, serverVersion)
// The following three options help with debugging, but should
// be changed or removed for production.
.LogTo(Console.WriteLine, LogLevel.Information);
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
);
}
}
For your posted code above, this translates to:
var connectionString = Configuration.GetConnectionString("DefaultConnection");
var serverVersion = ServerVersion.AutoDetect(connectionString);
services.AddDbContext<ApplicationDbContext>(options =>
options.UseMySql(connectionString, serverVersion));
We added a mandatory ServerVersion parameter to the UseMySql() method in 5.0, because Pomelo will make use of newer features, depending on the MySQL/MariaDB version you are using.

SignalR Core doesn't seem to respect Newtonsoft's TypeNameHandling setting

I'm passing around classes that derive from a common class. I'm finding that, despite passing deserialization valid data, the hub does not respect the TypeNameHandling and completely ignores the $type of the JSON. It deserializes to the base class regardless of what I try.
I event went and took the JSON that I was sending to the hub, pasted it into the hub code as a string, then used JsonConvert.Deserialize to see what would happen and it correctly deserialized to my derived object.
In my startup, I have
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
};
I wasn't sure if this was actually being used, so I had created a test JSON converter and put a debug on the CanRead property (which I had return a static false). That got hit. This also allowed my string deserialization to work.
So what is the hub doing differently that everything seems to work except the hub?
despite passing deserialization valid data, the hub does not respect the TypeNameHandling and completely ignores the $type of the JSON. It deserializes to the base class regardless of what I try.
I did a test with following code snippet, which work for me, you can refer to it.
In Startup:
services.AddSignalR().AddJsonProtocol(options => {
options.PayloadSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto
};
});
Hub method:
public async Task SendMessage1(Business mes)
{
//code logic here
//...
}
Classes:
public abstract class Business
{
public string Name { get; set; }
}
public class Hotel : Business
{
public int Stars { get; set; }
}
On client, send following JSON data to above hub method:
var ht = { "$type": "MyNamespaceHere.Hotel, NotesRTMSignalR", "Stars": 4, "Name": "Hudson Hotel" };
Test Result:

Automapper - Map fails after profile is added

I am using mapping by convention and have issues when adding profile to the configuration. Consider configuration below. I am using AutoFac to resolve profiles in CoreMapper and the profiles are correctly injected.
Mapping configuration
public CoreMapper(IEnumerable<Profile> profiles)
{
MapperConfiguration = new MapperConfiguration(cfg =>
{
cfg.CreateMissingTypeMaps = true;
cfg.AllowNullCollections = true;
cfg.AllowNullDestinationValues = true;
cfg.ForAllMaps((mapType, mapperExpression) =>
{
mapperExpression.PreserveReferences();
mapperExpression.MaxDepth(10);
});
cfg.IgnoreUnmapped();
foreach (var profile in profiles)
{
cfg.AddProfile(profile);
}
cfg.CreateMap<string, string>().ConvertUsing(str => string.IsNullOrEmpty(str) ? null : str);
});
Mapper = MapperConfiguration.CreateMapper();
}
IgnoreUnmapped is as follows:
private static void IgnoreUnmappedProps(TypeMap map, IMappingExpression expression)
{
foreach(var propName in map.GetUnmappedPropertyNames())
{
if (map.SourceType.GetProperty(propName) != null)
expression.ForSourceMember(propName, opt => opt.Ignore());
if (map.DestinationType.GetProperty(propName) != null)
expression.ForMember(propName, opt => opt.Ignore());
}
}
public static void IgnoreUnmapped(this IProfileExpression profile)
{
profile.ForAllMaps(IgnoreUnmappedProps);
}
In my code I have a generic class implementation with the line like this:
mapper.Map<TPoco>(entity);
Note: mapper is an instance of CoreMapper from above.
where TPoco is POCO model defined like:
public class ModelPoco : IModel {
// props
}
and entity is database entity model.
The mapping works fine. The result of mapper.Map< TPoco>(entity) is correct.
I then proceed and add a profile for a specific map that is not at all related to the ModelPoco being mapped before.
The profile being added:
public class RepositoryLayerProfile : AutoMapper.Profile {
public RepositoryLayerProfile() {
CreateMap<SomeOtherEntity, ISomeOtherModelInterface>();
}
}
Version: 6.2.2
Expected behavior
Mapping of ModelEntity to ModelPoco should happen normally.
Actual behavior
The mapping breaks with the message:
Unable to cast object of type 'Proxy< IModel>' to type 'ModelPoco'. on line mapper.Map(entity);
I am not sure why does it create Proxy class for IModel interface and then tries to cast to concrete implementation?
I explicitly set that I want to map entity to a concrete implementation.
If I remove CreateMap from the profile it works fine, but the first time I define CreateMap in the said profile, it breaks.
It seems to me like it forgets about the configuration for some reason even tho it has nothing to do with it.

Access appsettings from an abstract class used in my test scripts

Working with asp.net core 2.0
I have created a BD test project.
I have an abstract base class structured like this:
[ExcludeFromCodeCoverage]
[Category("Integration")]
public abstract class BaseIntegrationContext : BaseIntegrationSetUp
{
protected MyDataBaseContext Context;
private IWebHostBuilder _webHostBuilder;
protected override void FixtureSetUp()
{
base.FixtureSetUp();
WebSetUp();
DbSetUp();
}
private void WebSetUp()
{
_webHostBuilder = new WebHostBuilder()
.UseStartup<Startup>();
}
private void DbSetUp()
{
base.FixtureSetUp();
//options
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var options = new DbContextOptionsBuilder<InformedWorkerDbContext>()
.UseSqlServer(config.GetConnectionString("DefaultConnection"))
.Options;
Context = new MyDataBaseContext(options);
}
}
unless I copy and add the appsettings.json to this test project I will obviously get the error that the json file cannot be found.
What is the accepted way to access appsettings.json from withing an abstract base class that use in a test project?
I have tried adding an entity model called ServiceSettings that maps to the json file in my web project:
public class ServiceSettings
{
public string DatabaseServerConnection { get; set; }
}
which i instantiate in startup.cs:
services.Configure<ServiceSettings>(Configuration.GetSection("ServiceSettings"));
My Json file looks like this:
{
"ServiceSettings": {
"DatabaseServerConnection": "Server=localhost;Initial,Catalog=InformedWorker;Integrated Security=True;MultipleActiveResultSets=true"
},
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Information"
},
"Console": {
"LogLevel": {
"Microsoft.AspNetCore.Mvc.Razor.Internal": "Warning",
"Microsoft.AspNetCore.Mvc.Razor.Razor": "Debug",
"Microsoft.AspNetCore.Mvc.Razor": "Error",
"Default": "Information"
}
}
}
}
and in my abstract class I do this:
IOptions<ServiceSettings> myOptions = Options.Create(new ServiceSettings()
{
});
but typing myOptions the intellisense only gives me 'Value' to work with..?
unless I copy and add the appsettings.json to this test project I will obviously get the error that the json file cannot be found.
This is cause you don't specify file location in
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
.AddJsonFile registers a JSON FileProvider and path parameter ("appsettings.json" in your case) is a Path relative to the base path stored in ConfigurationBuilder.Properties. So the default base path is null and a file is expected in current working directory.
To set base path you may use .SetBasePath(<basePath>) extension method
public static IConfigurationBuilder SetBasePath(this IConfigurationBuilder builder, string basePath);
where basePath: The absolute path of file-based providers.

Castle Windsor: A better way to implement 2 levels of (nested) factories?

We have a pattern we've used several times, whereby we implement handlers and factories in separate Dlls. We configure exe's at runtime saying what dlls are loaded, and therefore what handlers are available to the app.
We do this because we have custom handling for some customers, also it allows great flexibility because we can quickly develop new handlers in isolation, and test and deploy them with confidence that we haven't even touched any other parts of a running application. We can also patch handlers by simply dropping in a single replacement dll, we have customers with strict change management procedures and they adore this.
To do this the pattern relies on two levels of factories, specific factories that implement specific handlers, and an overarching factory (which we call a Provider). The Provider chooses which handler factory to use to create a handler.
The question: Does Windsor contain something that would simplify this process for us?
Specifically I'm looking for something that could omit the Handler factory objects, it feels like something it should be able to do.
I've read up on the Typed Factory Facility and the UsingFactory & UsingFactoryMethod methods, but I can't see how they'd be any help here.
That said I often find the Castle Windsor documentation obtuse so I could be missing something obvious
Or is there just a better way of getting the same end goal that I haven't considered.
Here's some code to illustrate, first message, handler and factory interfaces
public interface IMessage
{
string MessageType { get; }
}
public interface IMessageHandler
{
void Process(IMessage message);
}
public interface IMessageHandlerFactory
{
bool CanProcessType(string type);
IMessageHandler Create();
}
In a second DLL we implement a handler and factory for Type1
public class Type1MessageHandler
: IMessageHandler
{
public void Process(IMessage message) { }
}
public class Type1MessageHandlerFactory
: IMessageHandlerFactory
{
public bool CanProcessType(string type)
{
return type == "Type1";
}
public IMessageHandler Create()
{
return new Type1MessageHandler();
}
}
In a third Dll we implement a handler and factory for Type2
public class Type2MessageHandler
: IMessageHandler
{
public void Process(IMessage message) { }
}
public class Type2MessageHandlerFactory
: IMessageHandlerFactory
{
public bool CanProcessType(string type)
{
return type == "Type2";
}
public IMessageHandler Create()
{
return new Type2MessageHandler();
}
}
In a windows service we implement the provider
public interface IMessageHandlerProvider
{
IMessageHandler Create(string messageType);
}
public class MessageHandlerProvider
: IMessageHandlerProvider
{
IEnumerable<IMessageHandlerFactory> factories;
public MessageHandlerProvider(IWindsorContainer wc)
{
factories = wc.ResolveAll<IMessageHandlerFactory>();
}
public IMessageHandler Create(string messageType)
{
foreach (var factory in factories)
if (factory.CanProcessType(messageType))
return factory.Create();
throw new UnableToFindMessageHandlerFactoryForType(messageType);
}
}
The service that actually needs the handlers only uses the Provider
public class MessageService
{
public MessageService(IMessageHandlerProvider handlerProvider) {}
}
What you are asking is indeed possible in Windsor with typed factories; instead of resolving all the factories in your provider and then looking for the ones that can process the message, you could ask Windsor for the handler that is linked to the message type and just use it. You don't really need the second level factory (IMessageHandlerFactory), because the handler can tell what message it will link to.
Here is a nice resource for this architecture (you've probably read this one already) which I'll summarize very quickly.
Given your interfaces, you start by registering all your handlers
container.Register(Classes.FromAssemblyInThisApplication()
.BasedOn<IMessageHandler>()
.WithServiceAllInterfaces());
Ok, now let's tell Windsor we want a factory that will return a IMessageHandler. What is nice is that we don't actually have to code anything for the factory.
container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>().AsFactory());
Now we can start using the factory
var provider = container.Resolve<IMessageHandlerProvider>();
var msg = new Type2Message();
var msgHandler = provider.Create(msg.MessageType);
The problem is that since there is no link between our message handlers and the string we pass to the factory, Windsor returns the first registered instance of a IMessageHandler it finds. In order to create this link we can name each message handler after the message type it is supposed to handle.
You can do it in a variety of ways, but I like to create a convention where a message handler type tells what messages it can handle:
container.Register(Classes.FromAssemblyInThisApplication()
.BasedOn<IMessageHandler>()
.WithServiceAllInterfaces().Configure(c => {
c.Named(c.Implementation.Name.Replace("MessageHandler", string.Empty));
}));
Now you need to tell your factory that the message type must be used as the name of the handler you want to resolve. To do that, it is possible to use a class inheriting the DefaulTypedFactoryComponentSelector. We just override the way component names are determined and return the message type we are receiving:
public class MessageHandlerSelector : DefaultTypedFactoryComponentSelector
{
protected override string GetComponentName(MethodInfo method, object[] arguments)
{
return arguments[0].ToString();
}
}
Now we can plug this selector in the factory
container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>()
.AsFactory(c =>c.SelectedWith(new MessageHandlerSelector())));
Here is the full code to handle any messages:
var container = new WindsorContainer();
container.Register(Classes.FromAssemblyInThisApplication()
.BasedOn<IMessageHandler>()
.WithServiceAllInterfaces().Configure(c => {
c.Named(c.Implementation.Name.Replace("MessageHandler", string.Empty));
}));
container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<IMessageHandlerProvider>().AsFactory(c =>c.SelectedWith(new MessageHandlerSelector())));
var provider = container.Resolve<IMessageHandlerProvider>();
var msg = new Type2Message();
var msgHandler = provider.Create(msg.MessageType);
msgHandler.Process(msg);
Here are some points I would like to underline:
as you guessed, you don't need the two factories: one is enough
the naming convention for the message handlers is not set in stone, and you could decide to have another mechanism in place to override the convention
I am not talking about releasing the components, but the links contain some info about it which you should look into
I didn't handle the case where no handler can be found, but Castle will throw by itself when it cannot resolve the handler with a ComponentNotFoundException
The system could perhaps be more robust if the handlers were explicit about the message types they handle. For example changing the interface to IHandlerOf<T> with T being a message type implementation.