Sitecore introduced Microsoft's Dependency Injection framework into the core CMS in Sitecore 8.2. This was exciting because Sitecore developers no longer needed to choose a dependency injection framework and plug it in themselves; dependency injection was available right out of the box. And it supported constructor injection into objects instantiated by the Configuration Factory (like pipeline processors), to boot.
Although Microsoft's Dependency Injection framework seemed to be as performant as alternatives that we wired up with Sitecore, we had to give up some amazing features from the alternatives, like Simple Injector's second-to-none diagnostic services. However, Microsoft introduced rudimentary service lifetime validation in .NET Core 2.0. Let's look at how to take advantage of this feature with the ASP.NET Core Rendering SDK, Sitecore 9.1 Initial Release, and newer to avoid hard-to-diagnose bugs in our solutions.
TL;DR
If you're developing on the ASP.NET Core Rendering SDK, this is enabled by default and you're good to go.
If you're developing on the core CMS, the code for this post is on GitHub: https://github.com/coreyasmith/sitecore-validate-services.
Features and Limitations
Andrew Lock has written a nice blog post on the .NET Core service validation features and limitations. The service validation is nowhere near as good as Simple Injector's but it's better than nothing. To recap a few of the key features, the .NET (Core) service provider can validate:
- That you don't have unregistered dependencies.
- That you don't inject scoped services into singleton services, creating captive dependencies.
- That your services can actually be instantiated (e.g., your constructors don't throw exceptions).
Unfortunately the scope validation feature only validates that you don't inject scoped services into singleton services; it doesn't validate that you don't inject transient services into scoped or singleton services, which can be just as problematic.
ASP.NET Core Rendering SDK
Service validation has been enabled by default for the ASP.NET Core Rendering SDK since the first version. All service registrations are validated immediately on startup of the Rendering Host when the service provider is built. If any services fail validation, an exception will be thrown that lists all of the issues found. By default, this feature is only enabled for the Development
environment.
You can easily test this feature in the Rendering Host. Create two new services and register them in ConfigureServices
of Startup.cs
:
public class ScopedService
{
}
public class SingletonService
{
public SingletonService(ScopedService service)
{
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ScopedService>();
services.AddSingleton<SingletonService>();
// ... rest omitted for brevity ...
}
Now when you start the Rendering Host, you should get the following exception:
'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: SingletonService Lifetime: Singleton ImplementationType: SingletonService': Cannot consume scoped service 'ScopedService' from singleton 'SingletonService'.)'
In the event that multiple services fail validation, the error screen will show all of the issues, which is quite nice.
Although I strongly recommend against it, you can disable this functionality in Program.cs
with the UseDefaultServiceProvider
extension method:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
.UseDefaultServiceProvider(options =>
{
options.ValidateOnBuild = false;
options.ValidateScopes = false;
});
}
Setting ValidateOnBuild
to false
will disable validation of your services on app start; however, if ValidateScopes
is set to true
, validation will still occur at runtime when services are instantiated.
Setting ValidateScopes
to false
will disable validation of scoped services into singleton services; however, if ValidateOnBuild
is set to true
, all other validations will still occur on app start.
Limitations
See Andrew Lock's blog post for a list of limitations. Read on for some limitations specific to the ASP.NET Core Rendering SDK.
Model-bound Views
Views cannot be verified at application startup. So if you use the @inject
attribute to inject services into your model-bound views, unregistered dependencies will only be caught at runtime when the model-bound view is rendered. Model-bound views are effectively transients, so lack of scope validation isn't a concern.
View Components
View Components cannot be verified at application startup. Unregistered dependencies injected through constructor injection to the ViewComponent
class, or injected with the @inject
attribute into the View, will only be caught at runtime when the View Component is rendered. View Components are effectively transients, so lack of scope validation isn't a concern.
Sitecore 9.1 Initial Release and higher
In Sitecore 9.1 Initial Release, Sitecore updated the Microsoft.Extensions.DependencyInjection
dependency to 2.1.1
, which came with a feature to validate service scopes. To take advantage of this functionality, create a ValidatingScopeProviderBuilder
.
For all of the following code you will need to have the Sitecore.Kernel
NuGet package installed. If you're not using package references, you will also need to install the Microsoft.Extensions.DependencyInjection
NuGet package appropriate for your version of Sitecore.
public class ValidatingServiceProviderBuilder : DefaultServiceProviderBuilder
{
protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton(serviceCollection);
return serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = true
});
}
}
This is an extension of the DefaultServiceProviderBuilder
that builds the service provider with scope validation enabled. It also adds the collection of service registrations (serviceCollection
) into the container as a service so we can retrieve and validate them.
Next, create a ValidateServices
pipeline processor for the HttpRequestBegin
pipeline to perform the service validation:
public class ValidateServices : HttpRequestProcessor
{
private static readonly Lazy<IServiceProvider> ServiceProviderFactory = new Lazy<IServiceProvider>(() =>
{
var serviceProviderBuilder = new ValidatingServiceProviderBuilder();
return serviceProviderBuilder.Build();
});
protected List<string> AssemblyPrefixes { get; } = new List<string>();
public override void Process(HttpRequestArgs args)
{
IEnumerable<Exception> exceptions;
var validatingServiceProvider = ServiceProviderFactory.Value;
using (var scope = validatingServiceProvider.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
var serviceCollection = serviceProvider.GetRequiredService<IServiceCollection>();
exceptions = ValidateServiceCollection(serviceProvider, serviceCollection).ToList();
}
if (!exceptions.Any())
{
return;
}
RenderExceptions(exceptions, args.HttpContext);
args.AbortPipeline();
}
private IEnumerable<Exception> ValidateServiceCollection(
IServiceProvider serviceProvider,
IServiceCollection serviceCollection)
{
var exceptions = new List<Exception>();
var validatableServices = serviceCollection.Where(IsValidatableService);
foreach (var service in validatableServices)
{
try
{
serviceProvider.GetRequiredService(service.ServiceType);
}
catch (Exception ex)
{
exceptions.Add(new Exception($"{service.ServiceType}", ex));
}
}
return exceptions;
}
private bool IsValidatableService(ServiceDescriptor descriptor)
{
return (!descriptor.ServiceType.IsGenericType || descriptor.ServiceType.IsConstructedGenericType)
&& AssemblyPrefixes.Any(prefix => descriptor.ServiceType.Assembly.FullName.StartsWith(prefix)
|| (descriptor.ImplementationType?.Assembly.FullName.StartsWith(prefix) ?? false)
|| (descriptor.ImplementationInstance?.GetType().Assembly.FullName.StartsWith(prefix) ?? false));
}
private static void RenderExceptions(IEnumerable<Exception> exceptions, HttpContextBase httpContext)
{
var sb = new StringBuilder();
sb.Append("<html><body>");
sb.Append("<h1>Some services failed validation.</h1>");
sb.Append("<table><tr><th>Service</th><th>Issue</th></tr>");
foreach (var exception in exceptions)
{
sb.Append("<tr>");
sb.Append($"<td>{exception.Message}</td>");
if (exception.InnerException != null)
{
sb.Append($"<td>{exception.InnerException.Message}</td>");
}
sb.Append("</tr>");
}
sb.Append("</table>");
sb.Append("</body></html>");
httpContext.Response.Write(sb.ToString());
httpContext.Response.ContentType = "text/html";
httpContext.Response.StatusCode = (int)HttpStatusCode.OK;
httpContext.Response.End();
}
}
This pipeline processor creates a ServiceProvider
that can validate service scopes using the ValidatingServiceProviderBuilder
above. Building the service provider is a slow operation, so the ServiceProvider
is cached for subsequent requests.
In the Process
method, a new scope is created by the ValidatingServiceProvider
so that validation of scopes can occur. The collection of services is pulled from the ValidatingServiceProvider
using serviceProvider.GetRequiredService<IServiceCollection>()
--this is possible because we registered the collection of services in the ValidatingServiceProviderBuilder
; normally there is no way to access all services registered with the container.
The service collection is then filtered only to validatable services. Generic types are filtered out because Microsoft does not validate them. Any services not in the AssemblyPrefixes
collection defined in config are filtered out as well. This is to ensure that only services in our assemblies are validated; we do not want to validate services from assemblies we don't own.
The filtered set of services is then requested one-by-one from the ServiceProvider
. If any of the services fail validation, they are added to a collection of Exception
s and then rendered in the response as a simple table that shows the name of the service that failed validation and the reason that the service failed validation.
Register the ValidateServices
processor at the beginning of the HttpRequestBegin
pipeline and specify the prefixes of the assemblies you want to validate:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor type="YourAssembly.DependencyInjection.Pipelines.HttpRequestBegin.ValidateServices, YourAssembly"
patch:before="*[1]">
<assemblyPrefixes hint="list">
<assemblyPrefix hint="yourassembly">YourAssembly</assemblyPrefix>
</assemblyPrefixes>
</processor>
</httpRequestBegin>
</pipelines>
</sitecore>
</configuration>
If you've registered Glass Mapper services with the container (maybe using my blog post on the subject), you will want to add Glass.Mapper
to the <assemblyPrefixes />
list.
Deploy the changes to your site. Either you will see the Some services failed validation
page, or your site will load normally. If your site loads normally, awesome, either you've not deployed the changes above correctly, or celebrate because you have no service registration issues (that Microsoft can validate, anyway).
If your site loads normally, you can validate that the ValidateServices
processor is working by adding <assemblyPrefix hint="sitecore">Sitecore</assemblyPrefix>
to your config and redeploying. As of Sitecore 10.3, Sitecore has a handful of invalid service registrations that will cause the Some services failed validation
page to display similar to this:
If you some of your services fail validation, fix them. Going forward you will catch service registration issues as soon as you introduce them with this processor in place so you won't push them to production.
⚠️WARNING⚠️: If you're working with Docker, disable this processor during initial build of your containers if you have invalid service registrations. The Some services failed validation
page will likely prevent the mssql-init
and solr-init
containers from working properly.
🚨DANGER🚨: Do not enable the ValidateServices
processor in any environment other than your local development environment. If you're working with Docker, create a custom development-only config patch to patch the processor in during local development.
Limitations
The limitations that Andrew Lock points out in his blog post still apply. Except, the Controller
limitation! In the .NET Framework you must register your Controller
s with the ServiceProvider
when you use constructor injection, which means your Controller
s will be validated.
Configuration Factory
Nothing instantiated by the Configuration Factory will be validated. Pipeline processors, event handlers, commands, any type you register in Sitecore config with resolve="true"
will not be validated. This is because those types are not registered directly with the container; the Configuration Factory instantiates those types through reflection and uses the service locator pattern to provide the constructor parameters.
Sitecore services
If you replace any of the built-in Sitecore types with your own, Sitecore types that depend on your replacements will not be verified. This is because the ValidateServices
processor by design does not validate Sitecore's services.
For example, if you registered a Scoped
implementation of the BaseLog
class, you'd be introducing a captive dependency all over Sitecore--many of Sitecore's singleton services depend on BaseLog
. ValidateServices
won't catch those issues, so be very careful when you're replacing Sitecore's built-in services. I recommend registering your custom service with the same lifetime as the original service.
If your services depend on a Sitecore service, that will still be validated. So if you register a singleton service that depends on a Sitecore scoped service, the ValidateServices
processor will catch that.
Replacing the DefaultServiceProviderBuilder
You may wonder, "why not replace Sitecore's DefaultServiceProviderBuilder
with the ValidatingServiceProviderBuilder
?" This would indeed be ideal. In theory we could even enable ValidateOnBuild
(Sitecore 10.1 or newer) like with the ASP.NET Rendering SDK. This would actually provide runtime validation of Configuration Factory types!
Unfortunately, it's not practical. When ValidateOnBuild
is enabled, the service provider is validated upon creation and we have no control over what types get validated (unlike with the ValidateServices
processor). Sitecore and several of its modules have service registration issues. This means we would have to fix and test these issues, for all configurations (e.g., XM1, XP0, XP1, XP Scaled), all roles (e.g., CM, CD, Processing), and all modules. This would be a massive undertaking just to get ValidateOnBuild
working.
Even if you did fix all of the issues thrown with ValidateOnBuild
enabled, you will still run into runtime issues with Configuration Factory types as you trigger certain pipelines, events, commands, etc. It will be a neverending game of Whac-A-Mole. You would also have to disable the ValidatingServiceProviderBuilder
when you install new modules as module installation could fail if the module has invalid service registrations.
I have filed a support ticket with Sitecore listing the issues that I have found in 10.3 and XM Cloud. Hopefully in a future version of Sitecore or XM Cloud ValidateScopes
and ValidateOnBuild
will be enabled by default like it is for the ASP.NET Core Rendering SDK!
Sitecore 8.2 to 9.0 Update-2
Sitecore 8.2 to 9.0 Update-2 use version 1.0.0
of Microsoft.Extensions.DependencyInjection
, so the ValidateScopes
feature is not available. However, you could still use the ValidateServices
pipeline processor with a slightly modified version of the ValidatingServiceProviderBuilder
to ensure that all of your services can indeed be instantiated:
public class ValidatingServiceProviderBuilder : DefaultServiceProviderBuilder
{
protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton(serviceCollection);
return base.BuildServiceProvider(serviceCollection);
}
}
🚨DANGER🚨: Do not install a higher version of Microsoft.Extensions.DependencyInjection
than 1.0.0
with these versions of Sitecore to try to enable ValidateScopes
. It is not a supported configuration and will break your instance.
Conclusion
Although not as comprehensive as Simple Injector's diagnostic services, the validate services features introduced in .NET Core 2.0 for Microsoft's Dependency Injection framework can help us avoid hard-to-diagnose bugs in our code. I think there's tremendous value in enabling these features in your solution from day one.
If you use this feature in one of your projects, let me know in the comments.
A big thanks to my friends Jean-François "Jeff" L'Heureux and Michael "PowerShell" West for being sounding boards for this blog post.