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:

  1. That you don't have unregistered dependencies.
  2. That you don't inject scoped services into singleton services, creating captive dependencies.
  3. 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 Exceptions 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:

sitecore-services-failed-validation

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 Controllers with the ServiceProvider when you use constructor injection, which means your Controllers 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.