I am trying to bind some configurable settings. The values are provided via the app-settings.local.json. The Model to bind to is called Configurable Settings. I have attempted to follow a few tutorials and troubleshoot the issue:
https://andrewlock.net/how-to-use-the-ioptions-pattern-for-configuration-in-asp-net-core-rc2/
https://github.com/aspnet/Configuration/issues/411
Cannot set Configuration from JSON appsettings file in .NET Core project
ServiceCollection returns null for IOptions even though GetSection is working
I have attempted to follow the advice given here and apply it in my own application. I could not get it to work after 4 hours of trying. Either I lack the basic knowledge needed to implement this or I'm overlooking something.
My ConfigurableSettings class is structured as follows:
public class ConfigurableSettings
{
public AppSettings _AppSettings;
public DgeSettings _DgeSettings;
public class AppSettings
{
[JsonProperty("SoftDelete")]
public bool SoftDelete { get; set; }
}
public class DgeSettings
{
[JsonProperty("zipFileUrl")]
public string zipFileUrl { get; set; }
[JsonProperty("sourceFileUrl")]
public string sourceFileUrl { get; set; }
}
}
My ConfigureServices is structured as follows:
public static void ConfigureServices(IServiceCollection serviceCollection, string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddOptions();
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("app-settings.local.json", true)
.AddJsonFile("app-settings.json", false)
.Build();
serviceCollection.Configure<ConfigurableSettings>(options => configuration.GetSection("AppSettings").Bind(options));
serviceCollection.Configure<ConfigurableSettings>(options => configuration.GetSection("DgeSettings").Bind(options));
var services = serviceCollection.BuildServiceProvider();
var options = services.GetService<IOptions<ConfigurableSettings>>();
serviceCollection.AddLogging(loggingBuilder =>
{
loggingBuilder.AddConfiguration(configuration.GetSection("Logging"));
loggingBuilder.AddConsole();
loggingBuilder.AddDebug();
});
serviceCollection.AddServices(configuration);
serviceCollection.AddNopCommerceServices(configuration);
serviceCollection.AddTransient<Comparator>();
serviceCollection.AddTransient<UpdateManager>();
serviceCollection.AddTransient<DgeRequestAuthenticator>();
serviceCollection.AddTransient<ISourceConnector, DgeConnector>();
serviceCollection.AddTransient<IDestinationConnector, NopCommerceConnector>();
}
My app-settings.local.json is configured as follows:
{
"AppSettings": {
"SoftDelete": true
},
"DgeSettings": {
"zipFileUrl" : "www.example-url.com",
"sourceFileUrl" : "www.example-url.com"
}
}
When I attempt to use it in a class, I call it in my constructor as follows:
private readonly ConfigurableSettings _settings;
public AlphaBetaService(IOptions<ConfigurableSettings> settings)
{
_settings = settings.Value;
}
Could someone help me to find out what I am doing wrong?
Not sure how do you use ConfigureServices method in your project, actually the ConfigureServices in Startup.cs must either be parameterless or take only one parameter of type IServiceCollection.
Change your ConfigureServices like below:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("app-settings.local.json", true)
.Build();
services.Configure<ConfigurableSettings>(configuration);
//...
}
}
Also change your model design like below:
public class ConfigurableSettings
{
public AppSettings AppSettings{ get; set; }
public DgeSettings DgeSettings { get; set; }
}
public class AppSettings
{
[JsonProperty("SoftDelete")]
public bool SoftDelete { get; set; }
}
public class DgeSettings
{
[JsonProperty("zipFileUrl")]
public string zipFileUrl { get; set; }
[JsonProperty("sourceFileUrl")]
public string sourceFileUrl { get; set; }
}
Related
I've been following the official Microsoft documentation and I've tried to enchance my codebase like the following:
public class GRequestModel(IConfiguration config) => _config = config;
{
private readonly IConfiguration Configuration;
public string path { get; set; }
public string secret { get; set; }
public string response { get; set; }
public string remoteip { get; set; }
public GRequestModel(string res, string remip)
{
response = res;
remoteip = remip;
var secret = Configuration["GoogleRecaptchaV3:Secret"];
var path = Configuration["GoogleRecaptchaV3:ApiUrl"];
if (String.IsNullOrWhiteSpace(secret) || String.IsNullOrWhiteSpace(path))
{
//Invoke logger
throw new Exception(
"Invalid 'Secret' or 'Path' properties in appsettings.json. " +
"Parent: GoogleRecaptchaV3.");
}
}
}
But I keep getting a lot of errors if I do it like it's written in the documentation...
To clarify - my goal is to read the secret and API URL from appsettings.json and use this to set the values for Google's reCAPTCHA v3.
I need to do this because I want to migrate my web app to a stable version of .NET 6.
Firstly, the syntax you have is wrong. Dependency injection should be done through the constructor (although some IoC containers provide property injection too):
public class GRequestModel
{
public string path { get; set; }
public string secret { get; set; }
public string response { get; set; }
public string remoteip { get; set; }
public GRequestModel(IConfiguration configuration, string res, string remip)
{
response = res;
remoteip = remip;
var secret = configuration["GoogleRecaptchaV3:Secret"];
var path = configuration["GoogleRecaptchaV3:ApiUrl"];
if (String.IsNullOrWhiteSpace(secret) || String.IsNullOrWhiteSpace(path))
{
//Invoke logger
throw new Exception("Invalid 'Secret' or 'Path' properties in appsettings.json. Parent: GoogleRecaptchaV3.");
}
}
}
Now we're accepting IConfiguration in the correct place, we can address how to create this object and get IConfiguration from the container. To do this, I'm going to create a delegate in the same GRequestModel class:
public class GRequestModel
{
public delegate GRequestModel Factory(string res, string remip);
Then we can register this factory with the container:
services.AddTransient<GRequestModel.Factory>(serviceProvider =>
(string res, string remip) => new GRequestModel(serviceProvider.GetRequiredService<IConfiguration>(), res, remip));
Now you can inject GRequestModel.Factory and create a GRequestModel using it:
public class SomeOtherClass
{
public SomeOtherClass(GRequestModel.Factory grequestFactory)
{
GRequestModel grm = grequestFactory("resValue", "remipValue");
}
}
Try it online
Edit: In response to your comment about it being more complicated than the documentation, that's because your use case is more complicated than the documentation examples. Specifically, you want to accept parameters res and remip.
Consider this example:
public class MyService
{
private readonly IConfiguration _configuration;
public MyService(IConfiguration configuration)
{
_configuration = configuration;
}
public void DoSomething(string res, string remip)
{
string configValue = _configuration["myConfigKey"];
}
}
You can register this and use this like so:
services.AddTransient<MyService>();
public class MyOtherClass
{
public MyOtherClass(MyService service)
{
service.DoSomething("resValue", "remipValue");
}
}
This is much simpler because you don't also need to inject the parameters into the constructor. It really depends on what you're trying to do as to which is the best option.
I added configurations to the appSettings.json file in my .NET Core project. For the sake of simplicy I'm taking database settings as an example. So in the settings file you would have
{
"Database": {
"Host": "localhost",
"Port": 1234,
"Database": "myDb",
"Username": "username",
"Password": "pw",
"EnablePooling": true
}
}
When configuring the services in the Startup.cs file I want to make those settings accessible via dependency injection. The data model for this is
public class DatabaseSettings
{
public string Host { get; set; }
public ushort Port { get; set; }
public string Database { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool EnablePooling { get; set; }
}
and I configure it this way
private void SetupSettings(IServiceCollection services)
{
ServiceProvider serviceProvider = services.BuildServiceProvider();
IConfiguration configuration = serviceProvider.GetService<IConfiguration>();
IConfigurationSection databaseConfigurationSection = configuration.GetSection("Database");
services.Configure<DatabaseSettings>(databaseConfigurationSection);
}
Lastly I want to validate those settings. I know that I can create a validator class implementing the IValidateOptions interface.
public class DatabaseSettingsValidator : IValidateOptions<DatabaseSettings>
{
private readonly IList<string> failures;
public DatabaseSettingsValidator()
{
failures = new List<string>();
}
public ValidateOptionsResult Validate(string databaseSettingsName, DatabaseSettings databaseSettings)
{
if (databaseSettings == null)
failures.Add($"{databaseSettingsName} are required.");
if (string.IsNullOrEmpty(databaseSettings?.Host))
failures.Add($"{nameof(databaseSettings.Host)} must not be empty.");
if (string.IsNullOrEmpty(databaseSettings?.Database))
failures.Add($"{nameof(databaseSettings.Database)} must not be empty.");
if (failures.Any())
return ValidateOptionsResult.Fail(failures);
return ValidateOptionsResult.Success;
}
}
but do I have to create this class and call the Validate method on my own? Maybe there is something like this sample code?
.
services.ValidateConfiguration<IOptions<DatabaseSettings>, DatabaseSettingsValidator>();
So you pass in the configured settings and the validator to use.
but I'm struggling with two questions:
Is there a way I can collect all failures instead of returning after
one? So you would get a list of failures instead of having to fix one
by one.
Do I have to create this class and call the Validate method on my own?
Maybe there is something like this sample code?
services.ValidateConfiguration<IOptions,
DatabaseSettingsValidator>(); So you pass in the configured settings
and the validator to use.
Yes, we could collect all failures list and display them at once, and we could also create a class which contains the Validate method. Please check the following steps:
First, since the class name is "DatabaseSettings", it better sets the config section name as the same as the class name:
{
"DatabaseSettings": {
"Host": "localhost",
"Port": 1234,
"Database": "myDb",
"Username": "username",
"Password": "pw",
"EnablePooling": true
}
}
[Note] If using a different name, the value might not map to the Database Setting class, so when validate the data, they all null.
Second, using the Data Annotations method adds validation rules to the model properties.
public class DatabaseSettings
{
[Required]
public string Host { get; set; }
[Required]
public ushort Port { get; set; }
[Required]
public string Database { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required]
public bool EnablePooling { get; set; }
}
Third, create a ServiceCollectionExtensions class which contains the ConfigureAndValidate method:
public static class ServiceCollectionExtensions
{
public static IServiceCollection ConfigureAndValidate<T>(this IServiceCollection #this,
IConfiguration config) where T : class
=> #this
.Configure<T>(config.GetSection(typeof(T).Name))
.PostConfigure<T>(settings =>
{
var configErrors = settings.ValidationErrors().ToArray();
if (configErrors.Any())
{
var aggrErrors = string.Join(",", configErrors);
var count = configErrors.Length;
var configType = typeof(T).Name;
throw new ApplicationException(
$"Found {count} configuration error(s) in {configType}: {aggrErrors}");
}
});
}
Then, register the ConfigureAndValidate service:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureAndValidate<DatabaseSettings>(Configuration);
}
Finally, get the Exception list.
public class HomeController : Controller
{
private readonly DatabaseSettings_settings;
public HomeController(IOptions<DatabaseSettings> settings)
{
_settings = settings.Value; // <-- FAIL HERE THROW EXCEPTION
}
}
Then, test result like this (I removed the Host and Username from the appSettings.json):
More detail information, you can check this blog:Validating configuration in ASP.NET Core
ValidateOptions are mainly for complex scenario, the purpose of using ValidateOptions is that you can move the validate logic out of startup.
I think for your scenario, you can use below code as a reference
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<MyConfigOptions>()
.Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
services.AddControllersWithViews();
}
For more details, please refer to this document
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1#options-validation
I use AutoMapper in my .NET CORE 2.2 project.
I get this exception:
Missing type map configuration or unsupported mapping.
Mapping types:
SaveFridgeTypeModel -> FridgeType
College.Refrigirator.Application.SaveFridgeTypeModel ->
College.Refrigirator.Domain.FridgeType
On This row:
var fridgeType = _mapper.Map<SaveFridgeTypeModel, FridgeType>(model);
Here is defenition of FridgeType class:
public class FridgeType : IEntity , IType
{
public FridgeType()
{
Fridges = new HashSet<Fridge>();
}
public int ID { get; set; }
//Description input should be restricted
public string Description { get; set; }
public string Text { get; set; }
public ICollection<Fridge> Fridges { get; private set; }
}
Here is defenition of SaveFridgeTypeModel class:
public class SaveFridgeTypeModel
{
public string Description { get; set; }
public string Text { get; set; }
}
I add this row:
services.AddAutoMapper(typeof(Startup));
To ConfigureServices function in Startup class.
UPDATE
I forgot to add mappin configuration to the post.
Here is mapping configs class:
public class ViewModelToEntityProfile : Profile
{
public ViewModelToEntityProfile()
{
CreateMap<SaveFridgeTypeModel, FridgeType>();
}
}
Any idea why I get the exception above?
You need to use the type from the assembly where your maps are when registering automapper with DI.
AddAutomapper(typeof(ViewModelToEntityProfile));
If you had multiple assemblies with maps - you could use another overload:
AddAutomapper(typeof(ViewModelToEntityProfile), typeof(SomeOtherTypeInOtherAssembly));
After creating mapping config class you need to add the AutoMapperConfiguration in the Startup.cs as shown below:
public void ConfigureServices(IServiceCollection services) {
// .... Ignore code before this
// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
mc.AddProfile(new ViewModelToEntityProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);
services.AddMvc();
}
I started some basic project on .net Core 1.1,
and I wish to map some properties from appsettings.json to object, but I probably can't understand correct name convention or something pretty basic
Regarding MSDN Using Options and configuration objects section,
it is very easy to use it.
I added next lines to appsettings.json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"XXXOptions": {
"X1": {
"AppId": "11",
"AppCode": "22"
},
"X2": {
"AppId": "",
"AppCode": ""
}
}
}
I added custom class
public class XXXOptions
{
public XXXOptions()
{
}
public X1 X1{ get; set; }
public X2 X2{ get; set; }
}
public class X1
{
public int AppId { get; set; }
public int AppCode { get; set; }
}
public class X2
{
public int AppId { get; set; }
public int AppCode { get; set; }
}
I added next code to Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Adds services required for using options.
services.AddOptions();
// Register the IConfiguration instance which MyOptions binds against.
services.Configure<XXXOptions>(Configuration);
// Add framework services.
services.AddMvc();
}
public class XXXController : Controller
{
private readonly XXXOptions _options;
public XXXController(IOptions<XXXOptions> optionsAccessor)
{
_options = optionsAccessor.Value;
}
public IActionResult Index()
{
var option1 = _options.X1;
return Content($"option1 = {option1.AppCode}, option2 = {option1.AppId}");
return View();
}
}
optionsAccessor.Value - Value containes null values at XXXController constructor.
but it seems like framework show mappet values at JsonConfigurationProvider inside of Configuration property
any ideas?
In ConfigureServices method change:
services.Configure<XXXOptions>(Configuration);
to
services.Configure<XXXOptions>(Configuration.GetSection("XXXOptions"));
I am encountering an error at Database creation at Application Start whereas the exact same code works perfectly fine in all other projects.
Startup function in Startup.cs
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
if (env.IsDevelopment())
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
Configuration = builder.Build();
Globals.Configuration = Configuration;
Globals.HostingEnvironment = env;
Globals.EnsureDatabaseCreated();
}
Globals.EnsureDatabaseCreated()
public static void EnsureDatabaseCreated()
{
var optionsBuilder = new DbContextOptionsBuilder();
if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:DataContext"]);
else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:DataContext"]);
else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:DataContext"]);
var context = new ApplicationContext(optionsBuilder.Options);
context.Database.EnsureCreated();
optionsBuilder = new DbContextOptionsBuilder();
if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:TransientContext"]);
else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:TransientContext"]);
else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:TransientContext"]);
new TransientContext(optionsBuilder.Options).Database.EnsureCreated();
}
ApplicationContext.cs
public class ApplicationContext : DbContext
{
public DbSet<Models.Security.User> Logins { get; set; }
public DbSet<Models.Security.Session> Sessions { get; set; }
public DbSet<Models.Security.Verification> VerificationTokens { get; set; }
public DbSet<Models.CRM.User> Users { get; set; }
public DbSet<Models.CRM.Organization> Merchants { get; set; }
public DbSet<Models.CRM.LinkedAddress> Shops { get; set; }
public DbSet<Models.CRM.ContactDetail> ContactDetails { get; set; }
public DbSet<Models.CRM.Location> Locations { get; set; }
public ApplicationContext(DbContextOptions options) : base(options)
{
}
}
Error Screenshot
After waiting for two days for an answer, unfortunately i ended up creating a new project and copying code there and it worked. Seems like a configuration issue.
Note: Since i didn't received any answers i am marking this as the correct answer. If a user comes in future and share their viewpoints, i will be happy to mark their answer if it adds some value to future readers.