Sometimes, during service registrations, I need to resolve other (already registered) services from the DI container. With containers like Autofac or DryIoc this was no big deal since you could register the service on one line and on the next line you could immediately resolve it.
But with Microsoft's DI container you need to register the service, then build a service provider and only then you are able resolve the services from that IServiceProvider
instance.
See the accepted answer this SO question: ASP.NET Core Model Binding Error Messages Localization
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
services.AddMvc(options =>
{
var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
var L = F.Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
options.ModelBindingMessageProvider.ValueIsInvalidAccessor =
(x) => L["The value '{0}' is invalid."];
// omitted the rest of the snippet
})
}
To be able to localize the ModelBindingMessageProvider.ValueIsInvalidAccessor
message, the answer suggests to resolve a IStringLocalizerFactory
through the service provider built based on the current service collection.
What is the cost of "building" the service provider at that point and are there any side effects of doing that, since the service provider will be built at least once more (after all services are added)?
BuildServiceProvider(IServiceCollection, ServiceProviderOptions) Creates a ServiceProvider containing services from the provided IServiceCollection optionally enabling service-creation and scope validation.
AddScoped(IServiceCollection, Type, Type) Adds a scoped service of the type specified in serviceType with an implementation of the type specified in implementationType to the specified IServiceCollection.
Each service provider has its own cache. Building multiple service provider instances can, therefore, lead to a problem called Torn Lifestyles:
When multiple [registrations] with the same lifestyle map to the same component, the component is said to have a torn lifestyle. The component is considered torn because each [registration] will have its own cache of the given component, which can potentially result in multiple instances of the component within a single scope. When the registrations are torn the application may be wired incorrectly which could lead to unexpected behavior.
This means that each service provider will have its own cache of singleton instances. Building multiple service providers from the same source (i.e. from the same service collection) will cause a singleton instance to be created more than once—this breaks the guarantee that there is at most one instance for a given singleton registration.
But there are other, just as subtle bugs that can appear. For instance, when resolving object graphs that contain scoped dependencies. Building a separate temporary service provider for the creation of an object graph that is stored in the next container might cause those scoped dependencies to be kept alive for the duration of the application. This problem is commonly referred to as Captive Dependencies.
With containers like Autofac or DryIoc this was no big deal since you could register the service on one line and on the next line you could immediately resolve it.
This statement implies that there are no problems with trying to resolve instances from the container while the registration phase is still in progress. This, however, is incorrect—altering the container by adding new registrations to it after you already resolved instances is a dangerous practice—it can lead to all sorts of hard to track bugs, independently of the used DI Container.
It is especially because of those hard to track bugs that DI Containers, such as Autofac, Simple Injector, and Microsoft.Extensions.DependencyInjection (MS.DI) prevent you from doing this in the first place. Autofac and MS.DI do this by having registrations made in a 'container builder' (AutoFac's ContainerBuilder
and MS.DI's ServiceCollection
). Simple Injector, on the other hand, does not make this split. Instead, it locks the container from any modifications after the first instance is resolved. The effect, however, is similar; it prevents you from adding registrations after you resolve.
The Simple Injector documentation actually contains some decent explanation on why this Register-Resolve-Register pattern is problematic:
Imagine the scenario where you want to replace some
FileLogger
component for a different implementation with the sameILogger
interface. If there’s a component that directly or indirectly depends onILogger
, replacing theILogger
implementation might not work as you would expect. If the consuming component is registered as singleton, for example, the container should guarantee that only one instance of this component will be created. When you are allowed to change the implementation ofILogger
after a singleton instance already holds a reference to the “old” registered implementation the container has two choices—neither of which are correct:
- Return the cached instance of the consuming component that has a reference to the “wrong”
ILogger
implementation.- Create and cache a new instance of that component and, in doing so, break the promise of the type being registered as a singleton and the guarantee that the container will always return the same instance.
For this same reason you see that the ASP.NET Core Startup
class defines two separate phases:
ConfigureServices
method), where you add registrations to the “container builder” (a.k.a. IServiceCollection
)Configure
method), where you state you want to use MVC by setting up routes. During this phase, the IServiceCollection
has been turned into a IServiceProvider
and those services can even be method injected into the Configure
method.The general solution, therefore, is to postpone resolving services (like your IStringLocalizerFactory
) until the “Use” phase, and with it postpone the final configuration of things that depend on the resolving of services.
This, unfortunately, seems to cause a chicken or the egg causality dilemma when it comes to configuring the ModelBindingMessageProvider
because:
ModelBindingMessageProvider
requires the use of the MvcOptions
class.MvcOptions
class is only available during the “Add” (ConfigureServices
) phase.IStringLocalizerFactory
and no access to a container or service provider and resolving it can’t be postponed by creating such value using a Lazy<IStringLocalizerFactory>
.IStringLocalizerFactory
is available, but at that point, there is no MvcOptions
any longer that you can use to configure the ModelBindingMessageProvider
.The only way around this impasse is by using private fields inside the Startup
class and use them in the closure of AddOptions
. For instance:
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(); services.AddMvc(options => { options.ModelBindingMessageProvider.SetValueIsInvalidAccessor( _ => this.localizer["The value '{0}' is invalid."]); }); } private IStringLocalizer localizer; public void Configure(IApplicationBuilder app, IHostingEnvironment env) { this.localizer = app.ApplicationServices .GetRequiredService<IStringLocalizerFactory>() .Create("ModelBindingMessages", "AspNetCoreLocalizationSample"); }
The downside of this solution is that this causes Temporal Coupling, which is a code smell of its own.
You could, of course, argue that this a ugly workaround for a problem that might not even exist when dealing with IStringLocalizerFactory
; creating a temporary service provider to resolve the localization factory might work just fine in that particular case. Thing is, however, that it is actually pretty hard to analyze whether or not you’re going to run in trouble. For instance:
ResourceManagerStringLocalizerFactory
, which is the default localizer factory, does not contain any state, it does takes a dependency on other services, namely IOptions<LocalizationOptions>
and ILoggerFactory
. Both of which are configured as singletons.ILoggerFactory
implementation (i.e. LoggerFactory
), is created by the service provider, and ILoggerProvider
instances can be added afterwards to that factory. What will happen if your second ResourceManagerStringLocalizerFactory
depends on its own ILoggerFactory
implementation? Will that work out correctly?IOptions<T>
—implemented by OptionsManager<T>
. It is a singleton, but OptionsManager<T>
itself depends on IOptionsFactory<T>
and contains its own private cache. What will happen if there is a second OptionsManager<T>
for a particular T
? And could that change in the future?ResourceManagerStringLocalizerFactory
is replaced with a different implementation? This is a not-unlikely scenario. What would the dependency graph than look like and would that cause trouble if lifestyles get torn?Unfortunately, when it comes to configuring the ModelBindingMessageProvider
, there seems no easy way out. This is IMO a design flaw in the ASP.NET Core MVC. Hopefully Microsoft will fix this in a future release.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With