I have an ASP.NET web api in ASP.NET core 2.1 and I have implemented a shared resource as explained here. This works fine.
The line :
services.AddLocalization()
adds localization to the inbuilt IOC container. (I think this is where the magic happens at least)
Now I have added Simple Injector into the mix for my own classes and I have a class which I have registered as Async Scoped which gets injected with the shared resource (through IStringLocalizer). However, IStringLocalizer is scoped as transient and this is not compatible with Async Scoped (as this has a longer scope). I can obviously solve the problem by setting the option "SuppressLifestyleMismatchVerification" to true, but this doesn't smell right. (in this case it probably doesn't matter but by using this option I mask any other problems I may have) Is there anyway of fixing this problem? Can I change the scoping of the shared resource for example?
TLDR; override the default registration as follows:
services.AddLocalization();
services.AddSingleton(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
What you are seeing is two universes collide; both DI libraries have their own, incompatible, definition of what it means to be transient:
Simple Injector considers components with a Transient
lifestyle to be short lived, or brief. That’s why Simple Injector doesn’t allow transients to be injected into Singleton components; a Singleton could live for a much longer time than would be considered brief.
Simple Injector versions before v5 (which is what OP was using) also restricted injection of transients services into scoped components. With the introduction of v5, however, this behavior as been loosened up a bit, which means transients can now (by default) be injected into scoped components. But still, transients can't be injected into singletons.
ASP.NET Core / MS.DI does not consider transient components to be short lived at all. Instead, the expected lifetime of a Transient
component in .NET Core is to be as long as its consumer's intended lifetime. The docs even state that "This lifetime works best for lightweight, stateless services."
It is because of this behavior that the Microsoft.Extensions.DependencyInjection (MS.DI) container allows injecting transients into both singleton and scoped consumers.
SIDE NOTE: I would even argue that Microsoft named their lifestyle incorrectly, because the actual behavior is to have one instance per consumer's dependency. Microsoft seems to have copied this behavior from Autofac. Autofac, however, does name this same lifestyle
InstancePerDependency
, which is a much more obvious name, if you ask me.
Weird thing, though, is that the Microsoft's AddLocalization
extension method registers StringLocalizer<T>
as transient. This is weird, because, apart from the wrapped IStringLocalizer
, StringLocalizer<T>
doesn't have any state. And not only that, the IStringLocalizer
that it wraps is produced by the injected IStringLocalizerFactory
and can be expected to be the same instance (which is enforced by the fact that the ResourceManagerStringLocalizerFactory
caches returned instances).
As stated above, within MS.DI, Transient
means “I'll live as long as my consumer does. “ This practically means that a StringLocalizer<T>
instance can live as long as a singleton, meaning: for the duration of the entire application.
From this respect it is actually really weird that the localization team chose StringLocalizer<T>
to have a transient lifestyle, even in MS.DI. Transient only means more instances created, and the IStringLocalizerFactory
invoked more often than required. I find Singleton
a much more obvious lifestyle for that registration.
I would, therefore, suggest overriding the default registration with a singleton, as it is safe to do so anyway:
services.AddLocalization();
services.AddSingleton(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));