Dependency injection from the box (Microsoft) has good stuffs, precisely IConfiguration interface, which allows to get stored configuration data using different providers and then to bind it with different classes using Options patterns. It's a really good stuff, but IConfiguration interface has only mechanism to read data and does not have to save data. From the other hand, usually, applications have possibility to change their configuration and to store it some where. Do you have any idea how I can implement this option?
Initially, I've stored some configuration data into the appsettings.json file and when I configure the DI container I get these data and bind their with particular class.
public static IHostBuilder ConfigureHost(this IHostBuilder builder)
{
_ = builder.ConfigureAppConfiguration((context, config) =>
{
_ = config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings12.json")
.Build();
});
_ = builder.ConfigureServices((context, services) =>
services.Configure<OptionsModel>(context.Configuration.GetSection("Options"))
.AddSingleton<IOptionsService, OptionsService>()
.AddSingleton<MainWindowModelView>()
.AddSingleton<MainWindow>()
);
return builder;
}
This code works fine, but I can't find is there any mechanism in the box to store the changed configuration data back. There is a form to change these configuration data and I want to save their to the file. Could anyone to suggest me how I can save it?
Related
This is an ASP.NET application in .NET 6. There's a wizard interface where the user inputs some data and then based on the input, I set up a new dependency injection container with the services that are required to complete the task. My problem is that the ILogger<> instances coming out of this second container don't use the custom ILoggingProvider that I set up.
Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddDebug();
builder.Logging.AddSignalRLogging();
public static class ILoggingBuilderExtensions
{
public static ILoggingBuilder AddSignalRLogging(this ILoggingBuilder builder)
=> builder.AddProvider(new SignalRLoggerProvider(builder.Services))
.AddFilter<SignalRLoggerProvider>("MyNamespace", LogLevel.Information);
}
The SignalRLoggerProvider comes from How to implement an ILogger to send messages to a SignalR Hub?.
Controller:
IServiceCollection services = new ServiceCollection();
services.AddLogging();
services.AddSignalR();
services.AddSingleton(_sheetsClient); // this was injected into the controller
services.AddSingleton<ITeamReaderService>(new PostedTeamReaderService(model.Divisions));
string[] divisionNames = model.Divisions.Keys.ToArray();
foreach (string divisionName in divisionNames)
{
services.AddSingleton<IDivisionSheetService>(provider => new DivisionSheetService(divisionName,
provider.GetRequiredService<StandingsRequestCreatorFactory>(),
provider.GetRequiredService<ISheetsClient>(),
provider.GetRequiredService<IOptionsMonitor<ScoreSheetConfiguration>>(),
provider.GetRequiredService<ILogger<DivisionSheetService>>())
);
}
I know my provider works because when I log things in a controller whose dependencies were injected from the HostBuilder's service collection (_sheetsClient), those messages work correctly. In classes that come from this other container (DivisionSheetService), those log messages go nowhere and when I view the ILogger instance in the debugger, it shows that it has no logger that it's writing to.
So it must be the case that my custom logging provider is unknown to the second container, but I can't figure out how to register it there.
Thanks in advance.
Since you're creating a new ServiceCollection from scratch, you also need to add the logging infrastructure from scratch:
IServiceCollection services = new ServiceCollection();
services.AddLogging(builder => builder.AddDebug().AddSignalRLogging());
This is my first time working with Microsoft.Extensions.Logging.Log4Net.AspNetCore.
I have a WPF application where I add the Log4Net provider within the CreateDefaultBuilder in App method of App.xaml.cs. Immediately after, I want to write to the log file using LogInformation saying "Starting Application". From what I can tell, it does not seem like I can do this because of the way it has been added to the Dependency Injection container. I must either call a method that accesses the DI container in the parameter list or add Log4Net to the ServiceProvider and then retrieve the service that way. But, that does not seem right because I will effectively have Log4Net added to DI twice.
Is there a way to immediately access the DI container after configuring Log4Net so I can write to the log file?
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, appBuilder) =>
{
// Do some stuff here
}).ConfigureLogging(logBuilder =>
{
logBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
logBuilder.AddLog4Net("log4net.config");
})
.ConfigureServices((context, services) =>
{
Configuration = context.Configuration;
ConfigureServices(Configuration, services);
})
.Build();
ServiceProvider = Host.Services;
// How to access Log4Net here?
_ISomeLogger.LogInformation("Starting Application");
Update
I am using the following example: https://www.thecodebuzz.com/logging-using-log4net-net-core-console-application/
The solution is to access the Log4Net from the ServiceProvider right after building it:
logger = (ILogger<Program>)ServiceProvider.GetService(typeof(ILogger<Program>));
In my .NET Core project, I have below settings in Configure method:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//services.AddOptions<UploadConfig>(Configuration.GetSection("UploadConfig"));
}
I have not registerd any IOptions and I am injecting it in a controller
[Route("api/[controller]")]
[ApiController]
public class HelloWorldController : ControllerBase
{
public HelloWorldController(IOptions<UploadConfig> config)
{
var config1 = config.Value.Config1;
}
}
The IOptions is getting resolved with the default instance, and I get to know error only when I am trying to use it (and when I expect the value to be not null).
Can I somehow get it to fail, stating the instance type is not registered or something similar? I just want to catch errors as early as possible.
The options framework is set up by the default host builder as part of its setup, so you do not need to AddOptions() yourself. This however also ensures that you can use IOptions<T> wherever you want since the framework will provide that exact options object for you.
The way options work is that the framework will always give you a T (as long as it can construct one). When you do set up configuration using e.g. AddOptions<T> or Configure<T>, what actually happens is that a configuration action gets registered for that type T. And when an IOptions<T> is later resolved, all those registered actions will run in the sequence they are registered.
This means that it’s valid to not have configured an options type. In that case, the default values from the object will be used. Of course, this also means that you are not able to detect whether you have actually configured the options type and whether the configuration is actually valid. This usually has to be done when you use the values.
For example, if you require Config1 to be configured, you should explicitly look for it:
public HelloWorldController(IOptions<UploadConfig> config)
{
if (string.IsNullOrEmpty(config.Value.Config1))
throw ArgumentException("Config1 is not configured properly");
}
An alternative would be to register a validation action for a type using OptionsBuilder.Validate. This will then be called automatically when you resovle the options object to validate the containing value. That way, you can have the validation set up in a central location:
services.AddOptions<UploadConfig>()
.Bind(Configuration.GetSection("UploadConfig"))
.Validate(c => !string.IsNullOrEmpty(c.Config1));
Unfortunately, this also means that you can only detect these problems when you actually use the values, which can be missed if you are not testing your application thoroughly. A way around this would be to resolve the options once when the application starts and validate them there.
For example, you could just inject your IOptions<T> within your startup’s Configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IOptions<UploadConfig> uploadOptions)
{
// since the options are injected here, they will be constructed and automatically
// validated if you have configured a validate action
// …
app.UseMvc();
}
Alternatively, if you have multiple options you want to validate and if you want to run logic that does not fit into the validation action, you could also create a service that validates them:
public class OptionsValidator
{
private readonly IOptions<UploadConfig> uploadOptions;
public OptionsValidator(IOptions<UploadConfig> uploadOptions)
{
_uploadOptions = uploadOptions;
}
public void Validate()
{
if (string.IsNullOrEmpty(_uploadOptions.Value.Config1))
throw Exception("Upload options are not configured properly");
}
}
And then inject that in your Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, OptionsValidator optionsValidator)
{
// validate options explicitly
optionsValidator.Validate();
// …
app.UseMvc();
}
Whatever you do, keep also in mind that by default the configuration sources are configured to support updating the configuration at run-time. So you will always have a situation in which a configuration can be invalid temporarily at run-time.
Great answer from poke, I just wanna complete it with this that you can fail-fast in your startup file when no configuration is provided:
public class MyOptions
{
public string MyValue { get; set; }
}
public void ConfigureServices(IServiceCollection services)
{
var options = Configuration.GetSection("MyOptions").Get<MyOptions>();
if (string.IsNullOrWhiteSpace(options?.MyValue))
{
throw new ApplicationException("MyValue is not configured!");
}
}
IOptions configuration values are read lazily. Although the
configuration file might be read upon application startup, the
required configuration object is only created when IOptions.Value is
called for the first time.
When deserialization fails, because of application misconfiguration,
such error will only appear after the call to IOptions.Value. This can
cause misconfigurations to keep undetected for much longer than
required. By reading -and verifying- configuration values at
application startup, this problem can be prevented.
This articles also helps you to get the idea:
Is IOptions Bad?
ASP.NET Core 2.2 – Options Validation
I am starting to work on an ASP.NET Core 2.0 Web Api. I added 2 secrets to the secrets.json file and am trying to read them in through the Configuration property in my Startup file. Each time I called I try to get the value from the Configuration variable, it returns null. An example of how I am reading this from the secrets.json is shown below.
public void ConfigureServices(IServiceCollection services)
{
var secret = Configuration["TestSecret"];
My secrets.json file looks like:
{
"TestSecret": "SecretValue"
}
I have also tried to retrieve the value by using:
public void ConfigureServices(IServiceCollection services)
{
IConfigurationSection secret = Configuration.GetSection("TestSecret");
var value = secret.Value;
This returns a section that corresponds to the TestSecret section, but the value in the IConfigurationSection is also null.
I have tried to install the Microsoft.Extensions.Configuration.UserSecrets NuGet package, but this hasn't helped. Am I missing a package I need to install or is there an alternate way to retrieve this value?
If I need to provide any more information to help solve this issue, please ask in the comments. I will try to provide as much information as possible to help solve this issue.
In general you use a file called "appSettings.json" file for storing all json values like that. This file doesn't need to be manually added in 2.0. Unless there is a specific reason for having multiple json files I would recommend doing this as it allows you to have all your application specific settings in the one place
it is possible to manually add a .json file though.
for asp.net Core 2.0 in your Program.cs in the BuildWebHost method
you add in the following
.ConfigureAppConfiguration((WebHostBuilderContext, ConfigurationBuilder) =>
{
ConfigurationBuilder
.AddJsonFile("Secrets.json", optional: true);
ConfigurationBuilder.AddEnvironmentVariables();
})
depending on the setup the entire method should look like
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((WebHostBuilderContext, ConfigurationBuilder) =>
{
ConfigurationBuilder
.AddJsonFile("Secrets.json", optional: true);
ConfigurationBuilder.AddEnvironmentVariables();
})
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
To get a value from in your ConfigureServices method you use
var testSecret = Configuration.GetSection("Secrets")["TestSecret"];
this is how the Secrets.Json file should look
{
"Secrets":
{
"TestSecret": "SecretValue"
}
}
Did you configure to use secrets in the Startup method?
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder();
if (env.IsDevelopment())
{
builder.AddUserSecrets<Startup>();
}
Configuration = builder.Build();
}
Read more here
For me it was null because I was playing around with other ASPNETCORE_ENVIRONMENT values and the default logic is apparently dependent on this being "Development"... which makes basic sense given that secrets.json is entirely local dev scenario.
As a related aside, for us there's mild heartburn that "Development" could mean either cloud runtime dev or raw local dev so we're forced to create yet another key/value to represent local dev because ASPNETCORE_ENVIRONMENT = Development drives specific logic we don't want to lose.
I'm a little bit confused here regarding how to inject DataProtection in ConfigureServices(IServiceCollection services) based on user settings being as well injected from services.Configure<UserSettingsConfig>(Configuration.GetSection("UserSettings"));.
The values appName_from_appsettings_json and dirInfo_from_appsettings_json below should be coming from the injected UserSettingsConfig and would be accessible anywhere else by injecting IOptions<UserSettingsConfig> but not here.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.Configure<UserSettingsConfig>(Configuration.GetSection("UserSettings"));
services.AddMvc();
services.AddScoped<DevOnlyActionFilter>();
services.AddDataProtection()
.SetApplicationName(appName_from_appsettings_json)
.PersistKeysToFileSystem(dirInfo_from_appsettings_json);
}
I've found ways to achieve my goals without using DI with code like var sharedDataProtectionAppName = configuration.GetValue<string>("UserSettings:SharedDataProtection:ApplicationName");
I had the feeling I have found the solution in this article http://andrewlock.net/access-services-inside-options-and-startup-using-configureoptions/ by it seems like I can't figure out how to apply it to my case. I would need a way to inject DataProtection based on values from the injected IOptions<UserSettingsConfig>. What would be the cleanest way to do that in your opinion?
UPDATE: I found a solution based on that type of code that I could be calling from ConfigureServices, but I still wonder if it's the best way.
var userSettingsConfig = services.BuildServiceProvider().GetServices<IOptions<UserSettingsConfig>>().First();
You could also use the extension method .Bind(). This method will try to bind the value to the Configuration object by matching the keys from the configuration.
// Add framework services.
var userSettingsConfig = new UserSettingsConfig();
Configuration.GetSection("UserSettings").Bind(userSettingsConfig);
services.Configure<UserSettingsConfig>(Configuration.GetSection("UserSettings"));
services.AddMvc();
services.AddScoped<DevOnlyActionFilter>();
services.AddDataProtection()
.SetApplicationName(userSettingsConfig.appName)
.PersistKeysToFileSystem(userSettingsConfig.DirInfo);