Starting with Sitecore 8.2, Sitecore has shipped with the .NET Core Dependency Injection container for dependency injection. As my friend Akshay Sura wrote on his blog, the performance of this container rivals Simple Injector, and today it's my preferred container when working with Sitecore. Plenty of posts have been written about working with dependency injection in Sitecore (read them all if you're just getting started), but in this post I'll cover how to work with a service lifetime that is finicky in Sitecore: Scoped
.
TL;DR
If you want to inject scoped services into anything created by the Sitecore Configuration Factory (e.g., pipeline processors), register them with the container like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddSingleton<Func<IScopedService>>(_ => () => ServiceLocator.ServiceProvider.GetService<IScopedService>());
}
}
Or grab the AddScopedWithFuncFactory
extension methods to register like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddScopedWithFuncFactory<IScopedService, ScopedService>();
}
}
In Configuration Factory instantiated classes that depend on your scoped service, inject like this:
public class ExampleProcessor
{
private readonly Func<IScopedService> _scopedServiceThunk;
public ExampleProcessor(Func<IScopedService> scopedServiceThunk)
{
_scopedServiceThunk = scopedServiceThunk ?? throw new ArgumentNullException(nameof(scopedServiceThunk));
}
public void Process(PipelineArgs args)
{
var scopedService = _scopedServiceThunk();
scopedService.DoSomething();
scopedService.DoSomethingElse();
}
}
Note that this only applies to types instantiated by the Configuration Factory. Scoped services can get injected into your Controller
s as IScopedService
; Func<IScopedService>
isn't necessary.
Service Lifetimes
All containers that I've used, including the .NET Core Dependency Injection container, support at a minimum three service scopes, or lifetimes:
- Singleton
- Scoped
- Transient
Singleton
Singleton is pretty self-explanatory. Any service registered as a singleton will only be created once during the lifetime of the application. That is, one instance of that type will be created, and that same type will be shared between all objects that depend on it. In general, this should be your go-to scope for best performance (but keep singleton services stateless!). In Sitecore you'd register like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<ISingletonService, SingletonService>();
}
}
Singleton services will only be disposed once the application shuts down.
Transient
Transient is the opposite of singleton. Every time a transient service is requested, a new instance of that service will be created. When would you want this? Your MVC and Web API controllers are good examples--you want a brand new MVC controller for every controller rendering on the page. In Sitecore you'd register a transient service like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<ITransientService, TransientService>();
}
}
Transient services are not disposed automatically. Most (if not all) containers work this way. Be careful if you register a transient service that implements IDisposable
--you need to dispose it yourself or you will cause memory leaks on your site.
Scoped
The focus of this post--the scoped lifetime--falls right inbetween singleton and transient. Services that are registered as scoped are instantiated once per request. It doesn't always have to be per request--you can define scopes however you like (e.g., per thread, your own scope), but in Sitecore (and web applications in general), "scoped" generally means request. In Sitecore you'd register a scoped service like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IScopedService, ScopedService>();
}
}
Scoped services that implement IDisposable
are automatically disposed at the end of each request.
Mixing Scopes
You're going to eventually register services of different scopes in your application. However, you need to be careful of the scopes of dependencies, otherwise you'll introduce captive dependencies that can cause difficult-to-diagnose bugs. Consider the following configurator:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<ISingletonService, SingletonService>();
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddTransient<ITransientService, TransientService>();
}
}
And the following classes:
public class ScopedService : IScopedService, IDisposable
{
private bool _disposed;
public void DoSomething()
{
if (_disposed) throw new Exception($"{nameof(ScopedService)} has been disposed.");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
}
}
public class SingletonService : ISingletonService
{
private readonly IScopedService _scopedService;
public SingletonService(IScopedService scopedService)
{
_scopedService = scopedService ?? throw new ArgumentNullException(nameof(scopedService));
}
public void DoSomething()
{
_scopedService.DoSomething();
}
}
As I mentioned above, singletons live for the entire application, but scoped services are disposed at the end of each request. On the first request to your application, the SingletonService
will get created, ScopedService
will get created, and SingletonService
will do its job happily. At the end of the request, ScopedService
will get disposed--it can't be used anymore. On the second request, the SingletonService
from the first request will get reused, and it'll throw an exception--it's trying to use a disposed object.
To avoid this issue follow this rule:
Services of shorter scopes can only depend on services of longer or equivalent scopes.
That means:
- Singleton services can only depend on other singleton services.
- Scoped services can only depend on singleton and other scoped services.
- Transient services can depend on any service scope.
In fact, one of Simple Injector's killer features is that it will check for violations of the above rules when your application starts and throw an exception if it detects any.
You can work around this limitation with the service locator, but it's an anti-pattern:
public class SingletonService : ISingletonService
{
public void DoSomething()
{
var scopedService = ServiceLocator.ServiceProvider.GetService<IScopedService>();
scopedService.DoSomething();
}
}
The above implementation will never have the disposal problems, because IScopedService
isn't cached in the SingletonService
. However, you're not going to enjoy the ceremony required to unit test this class.
Scoped Services in Sitecore
Dependency Injection and the Configuration Factory
Dependency Injection in Sitecore 8.2 was really exciting because it meant we could finally use constructor injection with types instantiated by the Configuration Factory (e.g., pipeline processors). Over were the days when we had to use service locator to inject dependencies. If you've not used constructor injection with a pipeline processor before, this is how you do it:
public SampleProcessor
{
private readonly ISingletonService _singletonService;
public SampleProcessor(ISingletonService singletonService)
{
_singletonService = singletonService ?? throw new ArgumentNullException(nameof(singletonService));
}
}
Then in config add the resolve="true"
attribute to your <processor />
node:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<anyPipeline>
<processor type="SampleAssembly.SampleProcessor, SampleAssembly"
resolve="true" />
</anyPipeline>
</pipelines>
</sitecore>
</configuration>
Don't forget that attribute or you'll get an exception like this:
Could not create instance of type: YourAssembly.YourProcessorType. No matching constructor was found.
This is an awesome feature, but there's one problem: most types instantiated by the Configuration Factory are singletons, pipeline processors included. Kevin Brechbühl wrote a nice post about this. So this seemingly limits constructor injection to only singleton services. Bummer.
In Kevin's post, Nick "Techphoria 414" Wesselman points out that if you add the reusable="false"
attribute to a <processor />
, it will be transient instead of singleton. Putting aside the fact that you probably shouldn't do this for performance reasons (especially in a pipeline like <httpRequestBegin />
), now you've got a way to support all service lifetimes in pipeline processors.
Except scoped.
Scoped Services and the Configuration Factory
As of Sitecore 9.1 Update-1 (all the way back to Sitecore 8.2 Initial Release), scoped services do not work with the Configuration Factory. If you add reusable="false"
to a pipeline processor and inject a scoped service into it, the scoped service will get treated as a singleton. Yes, it sounds crazy, but it's easy to test. Register the following service as scoped and inject it into an <httpRequestBegin />
processor with reusable="false"
:
public ScopedService
{
public Guid Id = Guid.NewGuid();
}
Put a breakpoint on Id
and watch its value over several requests--it'll always be the same. If this were behaving as a scoped service, its value would be different between each request. For even more fun, inject this service into a Controller
and notice that the instance injected into your Controller
has a different Id
than the instance injected into the processor! 🤯
If you dive into the Sitecore.Kernel
with dotPeek, you'll see that the Sitecore.Configuration.DefaultFactory
has an instance of IServiceProvider
injected into it. When Sitecore starts, the IServiceProvider
that gets injected here is the root service provider. What do I mean by root service provider? Sitecore MVP Dmytro Shevchenko touches on this in one of his many answers on Sitecore Stack Exchange. In pseudocode, this is how the .NET Core Dependency Injection container works in Sitecore at a high level:
// Application starts
using (var rootProvider = new ServiceProvider())
{
var configFactory = new DefaultFactory(rootProvider);
var serviceScopeFactory = new ServiceScopeFactory(rootProvider);
// Request begins
// SitecorePerRequestScopeModule HTTP module creates a new scope
using (var scopedProvider1 = serviceScopeFactory.CreateScope())
{
// controllers use scopedProvider to get services
// ServiceLocator.ServiceProvider uses scopedProvider to get services
// Configuration Factory types use rootProvider to get services
} // all services created by scopedProvider disposed
// Request ends
// Request begins
// SitecorePerRequestScopeModule HTTP module creates a new scope
using (var scopedProvider2 = serviceScopeFactory.CreateScope())
{
// controllers use scopedProvider to get services
// ServiceLocator.ServiceProvider uses scopedProvider to get services
// Configuration Factory types use rootProvider to get services
} // all services created by scopedProvider disposed
// Request ends
} // all services created by rootProvider disposed
// Application shuts down
Singletons are always created by the root provider (even when requested through a scoped provider). Scoped services get their "scoped" functionality by being created and disposed by a scoped provider. When the Configuration Factory is instantiated, it's provided a copy of the root provider, so everything injected by the Configuration Factory is effectively scoped to the application. The concept of a scoped service doesn't work--services registered as scoped behave as singletons with the root provider.
This has the unfortunate consequence that any IDisposible
scoped services resolved by the container won't be disposed until Sitecore shuts down which could result in memory leaks or other issues.
Injecting Scoped Services with the Configuration Factory
You can register almost any type with the .NET Core Dependency Injection container. This means that you can register Func<T>
(i.e., a method), and the container can inject that Func<T>
(method) into classes for you. We can take advantage of this to safely inject scoped services with the Configuration Factory.
Registering a Scoped Service
As I mentioned earlier, you can use ServiceLocator.ServiceProvider.GetService<T>()
in your pipeline processor Process
method to safely get access to scoped services. You can use the ServiceLocator.ServiceProvider
to inject scoped services by registering them twice like this:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddSingleton<Func<IScopedService>>(_ => () => ServiceLocator.ServiceProvider.GetService<IScopedService>());
}
}
Line 6 looks a little wild so let's break it down.
A Func<T>
is just a method--it's a method that takes no parameters and returns an object of type T
. Here's an example:
Func<int> someOtherMethod = SomeOtherMethod;
var result = someOtherMethod();
Console.WriteLine(result); // 4
public int SomeOtherMethod()
{
return 2 + 2;
}
Written more succinctly with a lambda:
Func<int> someOtherMethod = () => 2 + 2;
var result = someOtherMethod();
Console.WriteLine(result); // 4
So serviceCollection.AddSingleton<Func<IScopedService>>(...)
just means that we are registering a method that takes no parameters and returns an object of type IScopedService
with the container.
Now let's look at what's inside the ...
, because it probably looks like it wouldn't even compile: _ => () => ServiceLocator.ServiceProvider.GetService<IScopedService>()
.
99% of the time when you register services with serviceCollection
, you're doing it like this: serviceCollection.AddSingleton<ISingletonService, SingletonService>()
. However, instead of providing SingletonService
as the second type parameter, you can actually pass in a factory function to AddSingleton
, AddScoped
, and AddTransient
. So if you wanted, you could write serviceCollection.AddSingleton<ISingletonService, SingletonService>()
as:
serviceCollection.AddSingleton<ISomeInterface>(serviceProvider => {
var someDependency = serviceProvider.GetService<ISomeDependency>();
return new SomeImplementation(someDependency);
});
When you pass a factory function, it always takes serviceProvider
as a parameter. Another way to write the above would be:
serviceCollection.AddSingleton<ISomeInterface>(CreateSomeImplementation);
private static ISomeInterface CreateSomeImplementation(IServiceProvider serviceProvider)
{
var someDependency = serviceProvider.GetService<ISomeDependency>();
return new SomeImplementation(someDependency);
}
This is a handy feature, but beware: the serviceProvider
that is passed into the AddSingleton
, AddScoped
, and AddTransient
methods may be the root provider!!!!!!!! This means you never want to use it to resolve a scoped service. Which brings us back to this lambda: _ => () => ServiceLocator.ServiceProvider.GetService<IScopedService>()
.
The _
is just shorthand for I don't care about this parameter. I could've written serviceProvider => () => ServiceLocator.ServiceProvider.GetService<IScopedService>
, but since I'm not using the serviceProvider
parameter, I prefer to write _ => ...
. Instead, we delegate to ServiceLocator.ServiceProvider
to get the service, because it's always going to use the scoped provider under the hood. The () => ServiceLocator.ServiceProvider.GetService<IScopedService>()
portion of the lambda is the actual Func<IScopedService>
being returned.
The Func<IScopedService>
type is registered as a singleton for two reasons: performance and flexibility. It doesn't have state, so there's no need to create the method more than once (performance). It's registered as a singleton so that it can be injected into a service of any scope (flexibility).
If all of these lambdas feel a bit too cryptic, you could rewrite the ExampleConfigurator
as follows:
public class ExampleConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddSingleton(ScopedServiceThunk);
}
private static Func<IScopedService> ScopedServiceThunk(IServiceProvider serviceProvider)
{
return GetScopedService;
}
private static IScopedService GetScopedService()
{
return ServiceLocator.ServiceProvider.GetService<IScopedService>();
}
}
Notice on line 11
that I'm returning GetScopedService
instead of GetScopedService()
--that's because I want to return the GetScopedService
method, not the result of the method.
With all of this, we can now inject Func<IScopedService>
into services.
Use the Scoped Service
To use your scoped service, just take Func<IScopedService>
as a constructor parameter (don't forget to add resolve="true"
when you patch the processor in):
public class ExampleProcessor
{
private readonly Func<IScopedService> _scopedServiceThunk;
public ExampleProcessor(Func<IScopedService> scopedServiceThunk)
{
_scopedServiceThunk = scopedServiceThunk ?? throw new ArgumentNullException(nameof(scopedServiceThunk));
}
public void Process(PipelineArgs args)
{
var scopedService = _scopedServiceThunk();
scopedService.DoSomething();
scopedService.DoSomethingElse();
}
}
Then in your Process()
method just call _scopedService()
and under the hood it'll call ServiceLocator.ServiceProvider.GetService<IScopedService>()
for you. You can use this same trick to inject transient dependencies into scoped or singleton services, too.
This is technically no different than the following, except that we've inverted control of the ScopedServiceThunk
method to the instantiator of the processor:
public class ExampleProcessor
{
public void Process(PipelineArgs args)
{
var scopedService = ScopedServiceThunk();
scopedService.DoSomething();
scopedService.DoSomethingElse();
}
private static IScopedService ScopedServiceThunk()
{
return ServiceLocator.ServiceProvider.GetService<IScopedService>();
}
}
Outside of the Configuration Factory, you don't need to do this. MVC and Web API aren't affected by this issue, so you can inject your service directly without the indirection provided by the Func
.
Register Easily with Extension Methods
Registering scoped services twice is a pain. Fortunately, extension methods can get rid of that ceremony entirely. Add the following class to your solution (maybe in a foundation Dependency Injection module):
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddScopedWithFuncFactory<TService, TImplementation>(
this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
return services.AddWithFuncFactory<TService, TImplementation>(ServiceLifetime.Scoped);
}
public static IServiceCollection AddScopedWithFuncFactory<TService>(
this IServiceCollection services,
Func<IServiceProvider, TService> factory)
where TService : class
{
return services.AddWithFuncFactory(factory, ServiceLifetime.Scoped);
}
public static IServiceCollection AddTransientWithFuncFactory<TService, TImplementation>(
this IServiceCollection services)
where TService : class
where TImplementation : class, TService
{
return services.AddWithFuncFactory<TService, TImplementation>(ServiceLifetime.Transient);
}
public static IServiceCollection AddTransientWithFuncFactory<TService>(
this IServiceCollection services,
Func<IServiceProvider, TService> factory)
where TService : class
{
return services.AddWithFuncFactory(factory, ServiceLifetime.Transient);
}
private static IServiceCollection AddWithFuncFactory<TService, TImplementation>(
this IServiceCollection services,
ServiceLifetime lifetime)
where TService : class
where TImplementation : class, TService
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (!Enum.IsDefined(typeof(ServiceLifetime), lifetime))
throw new InvalidEnumArgumentException(nameof(lifetime), (int) lifetime, typeof(ServiceLifetime));
services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime));
return services.AddFuncFactory<TService>();
}
private static IServiceCollection AddWithFuncFactory<TService>(
this IServiceCollection services,
Func<IServiceProvider, TService> factory,
ServiceLifetime lifetime)
where TService : class
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (factory == null) throw new ArgumentNullException(nameof(factory));
if (!Enum.IsDefined(typeof(ServiceLifetime), lifetime))
throw new InvalidEnumArgumentException(nameof(lifetime), (int)lifetime, typeof(ServiceLifetime));
services.Add(new ServiceDescriptor(typeof(TService), factory, lifetime));
return services.AddFuncFactory<TService>();
}
private static IServiceCollection AddFuncFactory<TService>(this IServiceCollection services)
where TService : class
{
if (services == null) throw new ArgumentNullException(nameof(services));
services.AddSingleton<Func<TService>>(_ => () => ServiceLocator.ServiceProvider.GetService<TService>());
return services;
}
}
Now registering a scoped service with Sitecore is as easy as this:
serviceCollection.AddScopedWithFuncFactory<IScopedService, ScopedService>();
Or for a factory function:
serviceCollection.AddScopedWithFuncFactory<HttpRequestBase>(_ => HttpContext.Current.Request);
And with that my friends, as Chief Inspector Clouseau would say, the case is sol-ved.
Why Inject Func<T>
?
Why do this instead of using service locator? Aside from it being an anti-pattern, it becomes pretty clear with unit testing. Let's go back to the service locator example:
public class ExampleProcessor
{
public void Process(PipelineArgs args)
{
var scopedService = ServiceLocator.ServiceProvider.GetService<IScopedService>();
scopedService.DoSomething();
scopedService.DoSomethingElse();
}
}
In a unit testing context, how are you going to get a mock of IScopedService
into the ExampleProcessor
? Sure, you can go through the ceremony of setting up the ServiceLocator.ServiceProvider
, but look at how easy it is with the Func<T>
method:
public void Process_DoesSomeStuff_WhenCalled()
{
// Arrange
var mockScopedService = new Mock<IScopedService>();
// set up the mock service
var processor = new ExampleProcessor(() => mockScopedService.Object);
var args = new PipelineArgs();
// Act
processor.Process(args);
// Assert
// assert that stuff worked
}
That's nice and easy!
Practical Scoped Service Examples
What are some things you'd commonly want to wire up as scoped? Here are a few ideas:
HttpRequestBase
so you don't have to mess withHttpContext.Current.Request
.HttpResponseBase
for the same reason above.HttpSessionBase
for the same reason above.- A wrapper class for
Tracker.Current.Contact
. - Glass Mapper contexts:
IMvcContext
,IRequestContext
, etc.
Note that HttpContextBase
is already registered with the container out of the box as a transient service in Sitecore (nice find by Michael "Sitecore Junkie" Reynolds). Do not re-register it as a scoped service or you'll break stuff.
Conclusion
It's interesting to note that Habitat (it's not a starter kit!) doesn't support the scoped lifestyle in its Dependency Injection module--it only supports transient and singleton. It'd be interesting to see the techniques here implemented in that module with a Lifestyle.Scoped
attribute.
This isn't a perfect solution:
- It's still possible you could accidentally inject
IScopedService
instead ofFunc<IScopedService>
into a Configuration Factory type and have some runtime exceptions in production. - Although the above extension methods reduce the ceremony of registering scoped services, it's still possible to accidentally register your scoped services with
AddScoped
and have a bad time. Though some containers, like Autofac, do thisFunc<T>
registration for you automatically, if you're using them in your solution. - Injection and registration of
Func<T>
isn't the most readable code--it will likely be confusing for devs who haven't seen this pattern before.
However, until Sitecore fixes this in the Configuration Factory, it's the best way to do dependency injection of scoped services with the Configuration Factory. And it's the only way to inject services of shorter lifetimes into services of longer lifetimes.