Manually trigger IOptionsMonitor<>.OnChange - asp.net-core

In ASP.NET Core 2.1, I use IOptionsMonitor<> and have it set up so I can successfully get events for when I change the appSettings.json file. So this is working.
What I want to do now is to manually change some values in my options, through code, and have that trigger all my monitors. Is this possible?

For IOptionsMonitor<Locations>, it only changes the value in memory and did not save back to appsettings.json. For a workaround, you will need to implement your own method to save the changes back to appsettings.json.
define IWritableOptions which inherits from IOptions
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
implement your own WritableOptions
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly IConfigurationRoot _configuration;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
IConfigurationRoot configuration,
string section,
string file)
{
_environment = environment;
_options = options;
_configuration = configuration;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
_configuration.Reload();
}
}
Configure IWritableOptions<T>
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var configuration = (IConfigurationRoot)provider.GetService<IConfiguration>();
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, configuration, section.Key, file);
});
}
}
Register in Startup
services.ConfigureWritable<Locations>(Configuration.GetSection("Locations"));
Use
public class OptionsController : Controller
{
private readonly IWritableOptions<Locations> _writableLocations;
public OptionsController(IWritableOptions<Locations> writableLocations)
{
_writableLocations = writableLocations;
}
public IActionResult Change(string value)
{
_writableLocations.Update(opt => {
opt.Name = value;
});
return Ok("OK");
}
}
It will fire the IOptionsMonitor<>.OnChange

Related

Controller Constructor with primitive parameters

In an attempt to refactor code, I have recently changed my Controller to be constructed by taking string instead of IConfiguration and that is coming from another controller.
StatusController.cs
public class StatusController : Microsoft.AspNetCore.Mvc.ControllerBase
{
protected StatusDAL status_dal;
public StatusController(string connectionString)
{
status_dal = new StatusDAL(connectionString);
}
}
This particular controller is being called by another controller
HVIRCalcController.cs
public class HVIRCalcController : BaseServicesController
{
private readonly string _connectionString;
public HVIRCalcController(IConfiguration config)
{
_connectionString = config.GetConnectionString("toolboxConnectionStrWPS");
}
[HttpPost]
public IActionResult PostTask(int service_id, [FromBody]HVIRServiceRequestParams hvir_params)
{
StatusAck sack = new HVIR_Calc_Engine(_connectionString).BatchCalc(hvir_params);
}
}
HVIRCalcEngine.cs
public class HVIR_Calc_Engine
{
private string _connectionString;
public HVIR_Calc_Engine(string connectionString)
{
//this.config = config;
_connectionString = connectionString;
status_dal = new StatusDAL(connectionString);
pg_dal = new PostgresDAL(connectionString);
}
public StatusAck BatchCalc(HVIRServiceRequestParams hvirparams)
{
StatusAck statusack = new StatusController(_connectionString).PostStatusRecord();
}
}
It gives me the error:
Unable to resolve service for type 'System.String' while attempting to activate 'WPS_WebAPI.Controllers.StatusController'
Is there a way to avoid passing IConfiguration from HVIRCalcController all the way to StatusController?

How to access Controller class variable value in data access layer?

Code in appsettings.json:
{
"Logging":{
"LogLevel":{
"Default":"Warning"
}
},
"AllowedHosts":"*",
"ConnectionStrings":{
"DefaultConnection":"Server=localhost;Port=5432;Database=CrudDataBase;User Id=xyy;Password=xyz123###;"
}
}
Code in HomeController:
[Route("api/[controller]/[action]")]
[ApiController]
public class EcoSystemHomeController : ControllerBase
{
EcoBusinessHome ebl= new EcoBusinessHome();
public string ConnectionString;
private readonly IConfiguration configuration;
public EcoSystemHomeController(IConfiguration config)
{
this.configuration = config;
}
[HttpGet]
public object GetServiceStatus()
{
try
{
ConnectionString = configuration.GetConnectionString("DefaultConnection");
JObject StatusObj = new JObject();
StatusObj.Add(ConnectionString);
return StatusObj;
}
catch
{
return null;
}
}
}
My app follows the three-layer architecture. I have a variable named ConnectionString inside EcoSystemHomeController. It holds the connection string value. I want to use this ConnectionString variable in the data access layer. How can I achieve it?
Reading AppSettings from AppSettings.json using IConfiguration interface
In the below example, the IConfiguration is injected in the Controller and assigned to the private property Configuration.
Then inside the Controller, the AppSettings are read from the AppSettings.json file using the GetSection function.
Test Code:
private IConfiguration Configuration;
public TestController(IConfiguration _configuration)
{
Configuration = _configuration;
}
public IActionResult Index()
{
//ConnectionStrings
string appName = this.Configuration.GetSection("ConnectionStrings")["DefaultConnection"];
return View();
}
Result:

Create database context from cookie and base path in Entity Framework Core

Postgres database has multiple schemes like company1, company2, ... companyN
Browser sends cookie containing scheme name . Data access operations should occur in this scheme. Web application user can select different scheme. In this case different cookie value is set.
Npgsql EF Core Data provider is used.
ASP NET MVC 5 Core application registers factory in StartUp.cs :
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddScoped<IEevaContextFactory, EevaContextFactory>();
....
Home controller tries to use it:
public class HomeController : EevaController
{
public ActionResult Index()
{
var sm = new SchemeManager();
sm.PerformInsert();
....
This throws exception since factory member is null. How to fix this ?
public interface IEevaContextFactory
{
EevaContext Create();
}
public class EevaContextFactory : IEevaContextFactory
{
private IHttpContextAccessor httpContextAccessor;
private IConfiguration configuration;
public EevaContextFactory(IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
{
this.httpContextAccessor = httpContextAccessor;
this.configuration = configuration;
}
public EevaContext Create()
{
var builder = new DbContextOptionsBuilder<EevaContext>();
var pathbase = httpContextAccessor.HttpContext.Request.PathBase.Value;
var scheme = httpContextAccessor.HttpContext.Request.Cookies["Scheme"];
var csb = new NpgsqlConnectionStringBuilder()
{
Host = pathbase,
SearchPath = scheme
};
builder.UseNpgsql(csb.ConnectionString);
return new EevaContext(builder.Options);
}
}
Scheme data acess methods:
public class SchemeManager
{
readonly IEevaContextFactory factory;
public SchemeManager(IEevaContextFactory factory)
{
this.factory = factory;
}
public SchemeManager()
{
}
public void PerformInsert()
{
using (var context = factory.Create())
{
var commandText = "INSERT into maksetin(maksetin) VALUES (CategoryName)";
context.Database.ExecuteSqlRaw(commandText);
}
}
}
var sm = new SchemeManager()
... will call the no-parameter constructor on SchemeManager so the IEevaContextFactory is not injected. You should inject your factory into your controller and pass it into your SchemeManager.
Remove your no-parameter constructor. It's not needed.
public class HomeController : EevaController
{
private IEevaContextFactor eevaFactory;
public HomeController(IEevaContextFactory factory)
{
eevaFactory = factory;
}
public ActionResult Index()
{
var sm = new SchemeManager(eevaFactory);
sm.PerformInsert();
....
}
}
Your other option is to put the SchemeManager in the DI container and then the DI container will auto-resolve IEevaContextFactory on the constructor and then just inject SchemeManager into your controller.
Either way, remove that no-parameter constructor.

.Net Core use new ApplicationBuilder instead of IApplicationBuilder from configure

I am attempting to create a "registry" so that just creating a new class implementing IServiceCollectionInitializer or IApplicationBuilderInitializer allows it to be loaded. Instead of having a giant start up class the registry would add those automatically.
My problem is I dont know how to make the app either use a new application builder or retrieve the the one given automatically without getting it from startup.
public class ServiceCollectionInitializerRegistry
{
private readonly IList<IServiceCollectionInitializer> _serviceCollectionInitializers;
private readonly IServiceCollection _serviceCollection;
private readonly IServiceProvider _serviceProvider;
public ServiceCollectionInitializerRegistry(IServiceCollection serviceCollection)
{
_serviceCollectionInitializers = new List<IServiceCollectionInitializer>();
_serviceCollection = serviceCollection;
_serviceProvider = serviceCollection.BuildServiceProvider();
}
public ServiceCollectionInitializerRegistry WithInitializers(
params IServiceCollectionInitializer[] initializers)
{
if (!initializers.Any())
{
return this;
}
foreach (var initializer in initializers)
{
_serviceCollectionInitializers.Add(initializer);
}
return this;
}
public ServiceCollectionInitializerRegistry WithAssemblyInitializers()
{
var assembly = Assembly.GetEntryAssembly();
var initializerTypes =
assembly.GetTypes()
.Where(type => typeof(IServiceCollectionInitializer).IsAssignableFrom(type));
if (!initializerTypes.Any())
{
return this;
}
foreach (var type in initializerTypes)
{
_serviceCollectionInitializers.Add((IServiceCollectionInitializer)Activator.CreateInstance(type));
}
return this;
}
public void Build()
{
var configuration = _serviceProvider.GetRequiredService<IConfiguration>();
foreach (var serviceCollectionInitializer in _serviceCollectionInitializers)
{
var logger = _serviceProvider.GetRequiredService<ILoggerProvider>()
.CreateLogger(serviceCollectionInitializer.GetType().AssemblyQualifiedName);
serviceCollectionInitializer
.Initialize(
configuration,
_serviceCollection, logger);
}
}
}
public class ExceptionHandlingInitializer : IServiceCollectionInitializer, IApplicationBuilderInitializer
{
public void Initialize(IConfiguration configuration, IServiceCollection services, ILogger logger)
{
services.AddSingleton<IExceptionMapper, ExceptionMapper>();
services.AddSingleton<IExceptionHandler, ExceptionHandler>();
}
public void Initialize(IConfiguration configuration, IApplicationBuilder builder, ILoggerFactory loggerFactory)
{
builder.UseExceptionHandlerMiddleware();
}
}

Which HttpConfiguration object do I need to access to create a custom HttpParameterBinding?

In this post, Mike Wasson states:
"Besides ParameterBindingAttribute, there is another hook for adding a custom HttpParameterBinding. On the HttpConfiguration object"
But I have three HttpConfiguration objects in my Web API app, namely:
public static void Register(HttpConfiguration config, IWindsorContainer container) <-- in WebApiConfig.cs
private static void MapRoutes(HttpConfiguration config) <-- ""
public static void ConfigureWindsor(HttpConfiguration configuration) <-- in Global.asax.cs
Which of these (config, config, or configuration) should I use (if any)?
UPDATE
I tried this, with a breakpoint on the "if" line:
public static void ConfigureWindsor(HttpConfiguration configuration)
{
_container = new WindsorContainer();
_container.Install(FromAssembly.This());
_container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel, true));
var dependencyResolver = new WindsorDependencyResolver(_container);
configuration.DependencyResolver = dependencyResolver;
if (configuration.Properties.Values.Count > 0) // <-- I put a Casey Jones here
{
object o = configuration.Properties.Values.ElementAt(configuration.Properties.Values.Count - 1);
string s = o.ToString();
}
}
...but I only hit that spot once, on the server starting up, but not when the client sent a request to it...there must be some event that gets fired when a server passes a request where the incoming URL can be examined...no?
Usually you do have only one instance of HttpConfiguration which is the one you get from GlobalConfiguration.Configuration.
Said so, that's how I plugged custom binders
In global.asax
var binderMappings = new Dictionary<Type, Type>
{
{typeof(YourModelType), typeof(YourModelTypeBinder)},
//....
};
config.Services.Add(
typeof(ModelBinderProvider),
new WindsorModelBinderProvider(container, binderMappings));
WindsorModelBinderProvider
public class WindsorModelBinderProvider : ModelBinderProvider
{
private readonly IWindsorContainer _container;
private readonly IDictionary<Type, Type> _binderMappings;
public WindsorModelBinderProvider(IWindsorContainer container, IDictionary<Type, Type> binderMappings)
{
_container = container;
_binderMappings = binderMappings;
}
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
IModelBinder binder = null;
if (_binderMappings.ContainsKey(modelType))
{
binder = _container.Resolve(_binderMappings[modelType]) as IModelBinder;
if (binder == null)
{
throw new ComponentNotFoundException(modelType);
}
}
return binder;
}
}
YourModelTypeBinder
public class YourModelTypeBinder : IModelBinder
{
public YourModelTypeBinder(IYourServiceToLoadYourModelType service)
{
//...
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
bindingContext.Model = YourCustomCodeToLoadYourModelTypeUsingTheConstructorDependecies(actionContext.Request);
return true;
}
private YourModelType YourCustomCodeToLoadYourModelTypeUsingTheConstructorDependecies(HttpRequestMessage requestMessage)
{
...
}
}
YourModelTypeBinder will be resolved by the container(see WindsorModelBinderProvider), so you need to registered it first.
After all that plumbing, your controller may have a parameter, among others, as following
[ModelBinder]YourModelType user