Core2 has a hook for validating options read from appsettings.json:
services.PostConfigure<MyConfig>(options => {
// do some validation
// maybe throw exception if appsettings.json has invalid data
});
This validation code triggers on first use of MyConfig, and every time after that. So I get multiple runtime errors.
However it is more sensible to run validation during startup - if config validation fails I want the app to fail immediately. The docs imply that is how it works, but that is not what happens.
So am I doing it right? If so and this is by design, then how can I change what I'm doing so it works the way I want?
(Also, what is the difference between PostConfigure and PostConfigureAll? There is no difference in this case, so when should I use either one?)
This has been discussed in this dotnet/runtime issue since 2018.
In .NET 6, a ValidateOnStart extension method has been added to Microsoft.Extensions.Hosting
You can use it this way:
services.AddOptions<MyOptions>()
.ValidateDataAnnotations()
.ValidateOnStart(); // Support eager validation
However, ValidateDataAnnotations still does not validate nested properties and this won't be fixed soon (Microsoft issue).
This NuGet package provides a ConfigureAndValidate<TOptions> extension method which validates options at startup.
It is based on Microsoft.Extensions.Options.DataAnnotations. But unlike Microsoft's package, it can even validate nested properties.
It is compatible with .NET Standard 2.0, .NET Core 3.1, .NET 5, .NET 6 and .NET 7.
Documentation & source code (GitHub)
TL;DR
Create your options class(es)
Decorate your options with data annotations
Call ConfigureAndValidate<T>(Action<T> configureOptions) on your IServiceCollection
ConfigureAndValidate will configure your options (calling the base Configure method), but will also check that the built configuration respects the data annotations, otherwise an OptionsValidationException (with details) is thrown as soon as the application is started. No misconfiguration surprise at runtime!
Use
ServiceCollection extension
services.ConfigureAndValidate<TOptions>(configureOptions)
Is syntactic sugar for
services
.AddOptions<TOptions>()
.Configure(configureOptions) // Microsoft
.ValidateDataAnnotationsRecursively() // based on Microsoft's ValidateDataAnnotations, but supports nested properties
.ValidateOnStart() // or ValidateEagerly() in previous versions
.Services
OptionsBuilder extensions
ValidateDataAnnotationsRecursively
This method register this options instance for validation of its DataAnnotations at the first dependency injection. Nested objects are supported.
ValidateOnStart (or ValidateEagerly in previous versions)
This method validates this options instance at application startup rather than at the first dependency injection.
Custom validation
You can combine with your own option validations:
services
.AddOptions<TOptions>()
.Configure(configureOptions)
//...
.Validate(options => { /* custom */ }, message)
.Validate<TDependency1, TDependency2>((options, dependency1, dependency2) =>
{
// custom validation
},
"Custom error message")
//...
.ValidateDataAnnotationsRecursively()
.ValidateOnStart()
Microsoft options validation documentation
There is no real way to run a configuration validation during startup. As you already noticed, post configure actions run, just like normal configure actions, lazily when the options object is being requested. This completely by design, and allows for many important features, for example reloading configuration during run-time or also options cache invalidation.
What the post configuration action is usually being used for is not a validation in terms of “if there’s something wrong, then throw an exception”, but rather “if there’s something wrong, fall back to sane defaults and make it work”.
For example, there’s a post configuration step in the authentication stack, that makes sure that there’s always a SignInScheme set for remote authentication handlers:
options.SignInScheme = options.SignInScheme ?? _authOptions.DefaultSignInScheme ?? _authOptions.DefaultScheme;
As you can see, this will not fail but rather just provides multiple fallbacks.
In this sense, it’s also important to remember that options and configuration are actually two separate things. It’s just that the configuration is a commonly used source for configuring options. So one might argue that it is not actually the job of the options to validate that the configuration is correct.
As such it might make more sense to actually check the configuration in the Startup, before configuring the options. Something like this:
var myOptionsConfiguration = Configuration.GetSection("MyOptions");
if (string.IsNullOrEmpty(myOptionsConfiguration["Url"]))
throw new Exception("MyOptions:Url is a required configuration");
services.Configure<MyOptions>(myOptionsConfiguration);
Of course this easily becomes very excessive, and will likely force you to bind/parse many properties manually. It will also ignore the configuration chaining that the options pattern supports (i.e. configuring a single options object with multiple sources/actions).
So what you could do here is keep your post configuration action for validation, and simply trigger the validation during startup by actually requesting the options object. For example, you could simply add IOptions<MyOptions> as a dependency to the Startup.Configure method:
public void Configure(IApplicationBuilder app, IOptions<MyOptions> myOptions)
{
// all configuration and post configuration actions automatically run
// …
}
If you have multiple of these options, you could even move this into a separate type:
public class OptionsValidator
{
public OptionsValidator(IOptions<MyOptions> myOptions, IOptions<OtherOptions> otherOptions)
{ }
}
At that time, you could also move the logic from the post configuration action into that OptionsValidator. So you could trigger the validation explicitly as part of the application startup:
public void Configure(IApplicationBuilder app, OptionsValidator optionsValidator)
{
optionsValidator.Validate();
// …
}
As you can see, there’s no single answer for this. You should think about your requirements and see what makes the most sense for your case. And of course, this whole validation only makes sense for certain configurations. In particular, you will have difficulties when working configurations that will change during run-time (you could make this work with a custom options monitor, but it’s probably not worth the hassle). But as most own applications usually just use cached IOptions<T>, you likely don’t need that.
As for PostConfigure and PostConfigureAll, they both register an IPostConfigure<TOptions>. The difference is simply that the former will only match a single named option (by default the unnamed option—if you don’t care about option names), while PostConfigureAll will run for all names.
Named options are for example used for the authentication stack, where each authentication method is identified by its scheme name. So you could for example add multiple OAuth handlers and use PostConfigure("oauth-a", …) to configure one and PostConfigure("oauth-b", …) to configure the other, or use PostConfigureAll(…) to configure them both.
On an ASP.NET Core 2.2 project I got this working doing eager validation by following these steps...
Given an Options class like this one:
public class CredCycleOptions
{
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int VerifiedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SignedMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int SentMinYear { get; set; }
[Range(1753, int.MaxValue, ErrorMessage = "Please enter a valid integer Number.")]
public int ConfirmedMinYear { get; set; }
}
In Startup.cs add these lines to ConfigureServices method:
services.AddOptions();
// This will validate Eagerly...
services.ConfigureAndValidate<CredCycleOptions>("CredCycle", Configuration);
ConfigureAndValidate is an extension method from here.
public static class OptionsExtensions
{
private static void ValidateByDataAnnotation(object instance, string sectionName)
{
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(instance);
var valid = Validator.TryValidateObject(instance, context, validationResults);
if (valid)
return;
var msg = string.Join("\n", validationResults.Select(r => r.ErrorMessage));
throw new Exception($"Invalid configuration for section '{sectionName}':\n{msg}");
}
public static OptionsBuilder<TOptions> ValidateByDataAnnotation<TOptions>(
this OptionsBuilder<TOptions> builder,
string sectionName)
where TOptions : class
{
return builder.PostConfigure(x => ValidateByDataAnnotation(x, sectionName));
}
public static IServiceCollection ConfigureAndValidate<TOptions>(
this IServiceCollection services,
string sectionName,
IConfiguration configuration)
where TOptions : class
{
var section = configuration.GetSection(sectionName);
services
.AddOptions<TOptions>()
.Bind(section)
.ValidateByDataAnnotation(sectionName)
.ValidateEagerly();
return services;
}
public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddTransient<IStartupFilter, StartupOptionsValidation<TOptions>>();
return optionsBuilder;
}
}
I plumbed ValidateEargerly extension method right inside ConfigureAndValidate. It makes use of this other class from here:
public class StartupOptionsValidation<T> : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
var options = builder.ApplicationServices.GetService(typeof(IOptions<>).MakeGenericType(typeof(T)));
if (options != null)
{
// Retrieve the value to trigger validation
var optionsValue = ((IOptions<object>)options).Value;
}
next(builder);
};
}
}
This allows us to add data annotations to the CredCycleOptions and get nice error feedback right at the moment the app starts making it an ideal solution.
If an option is missing or have a wrong value, we don't want users to catch these errors at runtime. That would be a bad experience.
There are easy way to validating with using IStartupFilter and IValidateOptions.
You can just put below code your ASP.NET Core project.
public static class OptionsBuilderExtensions
{
public static OptionsBuilder<TOptions> ValidateOnStartupTime<TOptions>(this OptionsBuilder<TOptions> builder)
where TOptions : class
{
builder.Services.AddTransient<IStartupFilter, OptionsValidateFilter<TOptions>>();
return builder;
}
public class OptionsValidateFilter<TOptions> : IStartupFilter where TOptions : class
{
private readonly IOptions<TOptions> _options;
public OptionsValidateFilter(IOptions<TOptions> options)
{
_options = options;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
_ = _options.Value; // Trigger for validating options.
return next;
}
}
}
And just chain the ValidateOnStartup method on OptionsBuilder<TOptions>.
services.AddOptions<SampleOption>()
.Bind(Configuration)
.ValidateDataAnnotations()
.ValidateOnStartupTime();
If you want to create custom validator for options class, checkout this article.
This has been implemented in .NET 6. Now you can just write the following:
services.AddOptions<SampleOption>()
.Bind(Configuration)
.ValidateDataAnnotations()
.ValidateOnStart(); // works in .NET 6
No need for external NuGet Packages or extra code.
See OptionsBuilderExtensions.ValidateOnStart<TOptions>
Below is a generic ConfigureAndValidate method to validate immediately and "fail fast".
To summarize the steps:
Call serviceCollection.Configure for your options
Do serviceCollection.BuildServiceProvider().CreateScope()
Get the options instance with scope.ServiceProvider.GetRequiredService<IOptions<T>> (remember to take the .Value)
Validate it using Validator.TryValidateObject
public static class ConfigExtensions
{
public static void ConfigureAndValidate<T>(this IServiceCollection serviceCollection, Action<T> configureOptions) where T : class, new()
{
// Inspired by https://blog.bredvid.no/validating-configuration-in-asp-net-core-e9825bd15f10
serviceCollection.Configure(configureOptions);
using (var scope = serviceCollection.BuildServiceProvider().CreateScope())
{
var options = scope.ServiceProvider.GetRequiredService<IOptions<T>>();
var optionsValue = options.Value;
var configErrors = ValidationErrors(optionsValue).ToArray();
if (!configErrors.Any())
{
return;
}
var aggregatedErrors = string.Join(",", configErrors);
var count = configErrors.Length;
var configType = typeof(T).FullName;
throw new ApplicationException($"{configType} configuration has {count} error(s): {aggregatedErrors}");
}
}
private static IEnumerable<string> ValidationErrors(object obj)
{
var context = new ValidationContext(obj, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(obj, context, results, true);
foreach (var validationResult in results)
{
yield return validationResult.ErrorMessage;
}
}
}
Currently I have a autofac module which receives a string as parameter, and that string is coming from another dependency.
Im trying to pass it using below code, my question is: Is this the right approach or there is a better/improved way to do it?
builder.RegisterBuildCallback(c =>
{
var configService = c.Resolve<IConfigurationService>();
var module = new LoggingModule(configService.GetConfigurationValue("LoggerName"));
module.Configure(c.ComponentRegistry);
});
Generally try to avoid build callbacks and trying to configure the container based on configuring the container. To be honest, I'm surprised this even works since the container and registry are effectively immutable.
It would be better to use a lambda registration to resolve things. Since we don't know what your logging module is doing, let's say right now it's this:
public class LoggingModule
{
private readonly string _name;
public LoggingModule(string name)
{
this._name = name;
}
protected override void Load(ContainerBuilder builder)
{
builder.RegisterInstance(new MyLogger(this._name));
}
}
Even if it's not exactly this, it's pretty easy to adapt something similar - the parameter is coming into the module and being used by a registration.
You could move that into the registration itself and remove the module entirely.
builder.Register(ctx =>
{
var configService = ctx.Resolve<IConfigurationService>();
var name = configService.GetConfigurationValue("LoggerName");
return new MyLogger(name);
}).SingleInstance();
This avoids having to "know the parameter up front" and also avoids trying to reconfigure the container. You still get to register the config in DI and resolve it like you want.
I'm using the Options pattern to configure my ASP.net Core 3.1 web app.
There are two options classes:
public class SystemOptions
{
public string RootPath { get; set; }
}
public class ModuleOptions
{
public string SubPath { get; set; }
// this should become something like RootPath + SubPath
public string FullPath { get; }
}
And the associated appsettings.json
{
"SystemOptions": {
"RootPath": "\\webdav"
},
"ModuleOptions": {
"SubPath": "\subdirformodule"
}
}
And in Startup:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<SystemOptions>(configuration.GetSection("SystemOptions"));
services.Configure<ModuleOptions>(configuration.GetSection("ModuleOptions"));
}
Now I would like to initialize the FullPath in ModuleOptions once during app startup.
Therefore I need access to the SystemOptions.RootPath from within the ModuleOptions.
Is this possible?
I tried the following:
I added an InitializeFullPath() method to the ModuleOptions:
public string InitializeFullPath(string basePath)
{
// concat basePath and SubPath and return
... return fullPath;
}
and tried to use this in ConfigureServices:
services.Configure<SystemOptions>(configuration.GetSection("SystemOptions"));
services.AddOptions<ModuleOptions>()
.Configure<SystemOptions>((s, m) => m.FullPath = m.InitializeFullPath(s.RootPath));
But all I get is:
"No service for type '...SystemOptions' has been registered."
later on when Startup.Configure() is executed.
(And by the time this error occured, the InitializeFullPath method has not been executed at all - a breakpoint set there was not hit.)
So I have two questions:
how can I use the content of one option object during initialization of the second option object?
When will the delegate that you can specify in Configure() be executed?
I am going to answer your second question first. The configuration delegate is invoked the first time the Value property of the IOptions<YourOptions> is invoked. This interface is registered as a singleton so it's a one-time only thing. For IOptionsMonitor/IOptionsSnapshot they are similarly invoked on every new instance of the options.
Now to your first question... You were close! This should work:
services.AddOptions<ModuleOptions>()
.Configure<IOptions<SystemOptions>>(
(mod, sys) => mod.FullPath = mod.InitializeFullPath(sys.Value.RootPath)
);
Note that we are using IOptions<SystemOptions> and .Value. The Configure method that is chained to AddOptions is not the same as the one directly on the service collection; the generic arguments are the dependent service types and the first parameter is the options type from AddOptions. So that means that you reversed the arguments to the delegate (the option being configured is the first parameter).
Another...option is to use the IConfigureOptions interface. I typically go this route and don't use the form you have shown, even for "simple" dependent configuration:
public ModuleOptionsConfigurator : IConfigureOptions<ModuleOptions>
{
private readonly SystemOptions _sys;
public ModuleOptionsConfigurator(IOptions<SystemOptions> opts)
=> _sys = opts.Value;
public void Configure(ModuleOptions mod)
{
mod.FullPath = mod.InitializeFullPath(_sys.RootPath);
}
}
Which you then register with DI like so:
services.Configure<SystemOptions>(configuration.GetSection("SystemOptions"));
services.Configure<ModuleOptions>(configuration.GetSection("ModuleOptions"))
// register the configurator
services.ConfigureOptions<ModuleOptionsConfigurator>();
This allows you to encapsulate any sort of configurarion logic into a class. You can take zero dependencies up to however many you need.
The IPostConfigureOptions<> interface works similarly, but will run after all other Configure callbacks and IConfigureOptions<> implementations (and allows you to act differently for named options). Based on your description, this may be the better interface:
public ModuleOptionsPostConfigurator : IPostConfigureOptions<ModuleOptions>
{
private readonly SystemOptions _sys;
public ModuleOptionsPostConfigurator(IOptions<SystemOptions> opts)
=> _sys = opts.Value;
public void PostConfigure(string name, ModuleOptions mod)
{
mod.FullPath = mod.InitializeFullPath(_sys.RootPath);
}
}
IPostConfigureOptions is registered the same way as IConfigureOptions:
// register the configurator
services.ConfigureOptions<ModuleOptionsPostConfigurator>();
You can also combine the two interfaces in one implementing class, which I have often found a case for.
See the official documentation for more information on the options patterns.
I'm using Autofac to register named instances. I have to translate xml transactions into objects.
First, I have an enum.
public enum TransactionType
{
Unknown = 0,
[XmlNode("MyNodeA")]
TypeA = 1,
[XmlNode("MyNodeA")]
TypeB = 2
}
I have a method that creates an IDictionary<string, TransactionType> using the XmlNode attribute on the enum.
Here is my autofac mapping
var mappings = TransactionTypeHelper.GetDictionary();
foreach (var mapping in mappings)
{
builder.Register(ctx => {
return mapping.Key;
})
.Named<TransactionType>(mapping.Value)
.InstancePerLifetimeScope();
}
Then, I have a TransactionTypeFactory for getting the TransactionType based on the xml node.
public TransactionType GetTransactionType(string rootNode)
{
return _container.Resolve<TransactionType>(rootNode?.ToLower());
}
My problem is that I want to pass through any unknown xml nodes as unknown transactions so that I can process new transactions without making any code changes. The problem is that _container.Resolve throws an error if the node passed in has not been registered.
What I want to do is make autofac return the enum default if the named instance is not found instead of throwing an error. The funny thing is, I have unit tests where this container is mocked, and they all pass, but Autofac specifically blows up on this call.
I know this question is rather old, but I'd like to share a solution I have learned in the meantime in the hopes it will help someone with the same issue.
With autofac, you can register a function that can resolve using logic.
First, you would register each named instance. In the question I was doing this with a helper and iterating through a collection, but the essence is to map each value of the enum to an instance.
builder.Register<TransactionAClass>(ctx =>
{
//get any instances required by ConcreteClass from the ctx here and pass into the constructor
return new TransactionAClass();
})
.Named<Interfaces.ITransactionInterface>($"{TransactionType.TypeA:f}")
.InstancePerLifetimeScope();
Once you have all your registrations for known values, then we register a resolver function.
builder.Register<Func<TransactionType, Interfaces.ITransactionInterface>>(ctx =>
{
//you must resolve the context this way before being able to resolve other types
var context = ctx.Resolve<IComponentContext>();
//get the registered named instance
return (type) =>
{
var concrete = context.ResolveNamed<Interfaces.ITransactionInterface>($"{type:f}");
if (concrete == null)
{
//return a default class or throw an exception if a valid registration is not found
return new TransactionAClass();
}
return concrete;
}
});
Then, you can use the resolver like this
public class MyClass
{
private readonly ITransactionInterface transaction;
public MyClass(Func<TransactionType, Interfaces.ITransactionInterface> transactionResolver)
{
transaction = transactionResolver.Invoke(TransactionType.TypeA);
}
}
As detailed in InstancePerApiControllerType not working, I am unable to use the InstancePerApiControllerType to configure my solution. The answer provided there works so long as I am directly injecting a ConnectionContext into the controller, or otherwise know that a class is only used by a specific controller. Unfortunately that is not the case in my situation:
ControllerA -> EngineA -> RepositoryA -> GenericEntityAccessor
ControllerB -> EngineB -> RepositoryB -> GenericEntityAccessor
The issue is when we come in through ControllerA, GenericEntityAccessor needs "string A" and from ControllerB it needs "string B".
Of course, the real situation is a little more complicated and there are some bad practices such as code that directly "news"-up a ConnectionContext (it's legacy code). I'm currently exploring providing another component that provides the connection string that is injected via Autofac and configured in the controller using Lazy, but the bad practices are causing problems there also (i.e. once I start to change things in the interface, all the dominoes start to fall over and I end up 15 classes later wondering how I got there).
Are there any patterns, techniques, etc. that address this type of thing? I can't imagine it's all that uncommon.
UPDATE:
To provide a few more specifics, since I'm having some trouble getting this to work, in general we have the following hierarchy, showing which scopes I've applied
Controller -> InstancePerApiRequest()
I*Repository -> ?
I*Manager -> ?
I*Builder -> ?
I*Adapter -> ?
ISqlServerConnectionContext -> ?
IConnectionContextCache -> InstancePerApiRequest()
I've got a number of components that directly take ISqlServerConntectionContext and I'm trying to provide it like so:
container.Register(c =>
{
var connectionContextCache = c.Resolve<IConnectionContextCache>();
var connection = (ISqlServerConnectionContext)connectionContextCache.CurrentConnectionContext;
return connection;
}).As<ISqlServerConnectionContext>().InstancePerDependency();
Unfortunately at that point I'm getting a null for CurrectConnectionContext. My guess at this point is I've got some component that isn't rooted from the controller and I'm currently going through the dependencies manually attempting to find it (AFAIK the isn't a way for my to find out which object triggered Autofac to attempt to provide the ISqlServerConnectionContext when I'm debugging).
UPDATE 2:
It turns out I did have some issues where I was registering things improperly, and creating a dependency on ISqlServerConnectionContext for DocumentController, even though it did not have one (this was created through the delegate for something it did depend on).
Now I've got a circular reference that I'm pretty sure I've created myself in the registrations:
container.Register(x =>
{
if (x.IsRegistered<HttpRequestMessage>())
{
var httpRequestMethod = x.Resolve<HttpRequestMessage>();
var tokenHelper = x.Resolve<ITokenHelper>();
var token = tokenHelper.GetToken(httpRequestMethod);
return token ?? new NullMinimalSecurityToken();
}
return new NullMinimalSecurityToken();
}).As<IMinimalSecurityToken>().InstancePerApiRequest();
container.Register(c =>
{
var connectionContextCache = c.Resolve<IConnectionContextCache>();
var token = c.Resolve<IMinimalSecurityToken>();
var connection = (ISqlServerConnectionContext)connectionContextCache.CurrentConnectionContext;
connection.Token = token;
return connection;
}).As<ISqlServerConnectionContext>().InstancePerApiRequest();
The problem is ISqlServerConnectionContext has a property of type IMinimalSecurityToken which is optional, and definitely not used when the ISqlServerConnectionContext is being used to look up IMinimalSecurityToken, which depends on ISqlServerConnectionContext through ITokenHelper.
UPDATE 3:
For completeness, in order to solve my circular reference problem I needed to use named services, and use a SqlServerConnectionContext that did not have the IMinimalSecurityToken property set for the IOAuthTokenManager registration. Now I'm getting the dreaded
No scope with a Tag matching 'AutofacWebRequest' is visible
error, but I think that warrants a new question if I'm not able to solve it.
container.Register(c =>
{
var productId = WellKnownIdentifierFactory.Instance.GetWellKnownProductIdentifier(WellKnownProductIdentifiers.RESTSearchService);
var connectionString = ConfigurationManager.AppSettings[AppSettingsNames.DatabaseConnection];
var newConnectionContext = new SqlServerConnectionContext(connectionString) { ProductID = productId };
newConnectionContext.Open();
return newConnectionContext;
}).Named<ISqlServerConnectionContext>("OAuthTokenConnectionContext").InstancePerApiRequest();
container.Register(c => new SqlServerBuilderFactory(c.ResolveNamed<ISqlServerConnectionContext>("OAuthTokenConnectionContext"))).Named<IBuilderFactory>("OAuthTokenBuilderFactory").InstancePerApiRequest();
container.Register(c =>new OAuthTokenManager(c.ResolveNamed<IBuilderFactory>("OAuthTokenBuilderFactory"))).As<IOAuthTokenManager>().InstancePerApiRequest();
This can be solved using AutoFac's support for object graph lifetime scoping.
Cache the current SqlServerConnectionContext in an object scoped to the lifetime of your controller.
Within the SqlServerConnectionContext factory type, once the connection is created assign it to the backing field of the current lifetime-scoped cache
Any types scoped within the lifetimes scope of a controller can then access the connection associated with that controller through the cache
The only complexities I can think of are:
If the controller is not actually the root of a lifetime scope for all types with a dependency on a specific connection. I.e. if they fall outside the lifetime of the controller.
If any of the dependencies are registered as single instance. In which case they will not be able to resolve the Cache as it is currently implemented as it is PerApiRequest.
For example:
public interface ISqlServerConnectionContextCache
{
ISqlServerConnectionContext CurrentContext { get; set; }
}
public class SqlServerConnectionContextScopeCache : ISqlServerConnectionContextCache
{
public ISqlServerConnectionContext CurrentContext { get; set; }
}
public interface ISqlServerConnectionContextFactory
{
ISqlServerConnectionContext Create();
}
// The factory has the cache as a dependancy
// This will be the first use of the cache and hence
// AutoFac will create a new one at the scope of the controller
public class SqlServerConnectionContextFactory : ISqlServerConnectionContextFactory
{
private string _connectionString;
private ISqlServerConnectionContextCache _connectionCache;
public SqlServerConnectionContextFactory(ISqlServerConnectionContextCache connectionCache,
string connectionString)
{
_connectionCache = connectionCache;
_connectionString = connectionString;
}
public ISqlServerConnectionContext Create()
{
var connectionContext = new SqlServerConnectionContext(_connectionString);
connectionContext.Open();
_sqlServerConnectionContextProvider.CurrentContext = connectionContext;
return connectionContext;
}
}
public class MyController : ApiController
{
private ISqlServerConnectionContext _sqlServerConnectionContext;
public MyController(Func<string, ISqlServerConnectionContextFactory> connectionFactory)
{
_sqlServerConnectionContext = connectionFactory("MyConnectionString");
}
}
// As the cache is lifetime scoped it will receive the single instance
// of the cache associated with the current lifetime scope
// Assuming we are within the scope of the controller this will receive
// the cache that was initiated by the factory
public class MyTypeScopedByController
{
public MyTypeScopedByController(ISqlServerConnectionContextCache connectionCache)
{
var sqlServerConnectionContext = connectionCache.CurrentContext;
}
}
// AutoFac wiring
builder.RegisterType<SqlServerConnectionContextScopeCache>()
.As<ISqlServerConnectionContextCache>()
.InstancePerApiRequest();
builder.RegisterType<SqlServerConnectionContextFactory>()
.As<ISqlServerConnectionContextFactory>()
.InstancePerDependency();