DALL·E: an oil painting of a computer singing a sea shanty

What can you do with an empty ServiceProvider?

Oh what can you do with an empty service provider,

what can you do with an empty service provider,

what can you do with an empty service provider,

so damn early in the morning!

Playing with an empty service provider is the best way to learn the basics of services, service collections, and service providers for Dependency Injection (DI). An empty service provider is the simplest possible case of a service-based architecture; free from all the distracting details of the actual services themselves. So while it's rare to ever encounter an empty service provider in the wild (since as the architectural heart of an application they are usually full of tens, hundreds, or thousands of services), it pays to spend some time closely examining a specimen.

For example, you might think it's obvious what an empty service provider can do: nothing. But looking closer, you'll see that even an empty service provider has a few tricks up its sleeve.

The companion code for this article on GitHub is here: https://github.com/davidcoats/D8S.E0003

And in a prior article we looked at how to create an empty service provider.

But let's review:

What is an empty service provider?

An empty service provider is a service provider built from a service collection that has no entries. When you create a new service collection it starts empty, just like any other collection (e.g. List<T>). If you immediately call the BuildServiceProvider() method, you get a service provider with no services, an "empty" service provider.

Here's some quick code to create one:


// Requires the Microsoft.Extensions.DependencyInjection NuGet package.

using Microsoft.Extensions.DependencyInjection;

static void CreateEmptyProvider()
{
    ServiceCollection serviceCollection = new ServiceCollection();

    ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

    // Now you have an empty service provider.
}

List of Operations

Let's get started: here are the different operations we'll look at. Note that while all of these *can* be done with an empty service provider, some will succeed (not result in an exception) and some will fail (result in an exception):

Operations that succeed

First, here are the operations that succeed, and by "succeed", I mean "does not throw an exception".

Request a non-existent service

The most important ability of a service provider is to tell you when a service is not available. You might think it's the ability to provide services, but if there was no way for the service provider to tell you a service is not available, how would you know the service you requested is real?

This is important because it's also entirely possible to request a service that does not exist. Either by accident or on purpose, you might ask for a service type that was not added to (i.e. registered with) the service collection from which a service provider was built.

So what exactly happens in this case? While details can differ based on the service provider implementation you are working with, the standard Microsoft.Extensions.DependencyInjection.ServiceProvider implementation returns null.


public void TryGetNonExistentService()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	var nonExistentService = serviceProvider.GetService<IService>();

	var isNull = nonExistentService is null;

	var nonExistentServiceRepresentation = isNull
		? "<null>"
		: nonExistentService.ToString()
		;

	// Result:
	// <null>: IService instance
	// The instance returned for a non-existent service is null.
	Console.WriteLine($"{nonExistentServiceRepresentation}: {nameof(IService)} instance");
}
    

The fundamental service provider method is IServiceProvider.GetService(Type), and calling it with a services type that has not been registered results in a null object. Btw, we are using the GetService<T>() extension method for clarity, but it's the same thing.

Now you know how to test if a service is available: is the result of asking for a service null?

Request an enumerable for a non-existentent service

What happens if there are multiple service implementation types registered for a single definition type?

The ServiceProvider type has a special ability for this case, and it provides an IEnumerable of the definition type. For example, say you had a file-based and a web-based implementation of some values-providing service:


public interface IValuesProviderService
{
	string[] GetValues();
}

public class FileBasedValuesProviderService : IValuesProviderService
{
	public string[] GetValues()
	{
		// Get a bunch of values from a file...
	}
}

public class WebBasedValuesProviderService : IValuesProviderService
{
	public string[] GetValues()
	{
		// Get a bunch of values by querying a Web-API...
	}
}
    

With:


services.AddService<IValuesProviderService, FileBasedValuesProviderService>();
services.AddService<IValuesProviderService, WebBasedValuesProviderService>();
    

If you were to ask for the service type IEnumerable<IValuesProviderService>, you would get an enumerable containing *both* implementations:


var allValuesProviderServices = serviceProvider.GetService(typeof(System.Collections.Generic.IEnumerable<IValuesProviderService>));
    

This is done more simply with the GetServices<T>() extension method:


var allValuesProviderServices = serviceProvider.GetServices<IValuesProviderService>();
    

And as another special ability, if you were to ask for only a single IValuesProviderService, you would get only the last implementation added to the collection. Given the sequence of adding services above, the result would be an WebBasedValuesProviderService:


WebBasedValuesProviderService webBasedValuesProviderServices = serviceProvider.GetService<IValuesProviderService>();
    

But if you were to switch the order, then you would get the other implementation.

This "last-if-multiple" behavior is useful when you want to override the behavior of a service added by a library. If the library helpfully added a default implementation of a service it depends on, by adding your own implementation after, the library will then use your implementation.

Great! But what happens if you ask for an enumerable of a service that doesn't exist?


public void TryGetEnumerableOfNonExistentService()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

    var nonExistentServices = serviceProvider.GetServices<IService>();

    var isNull = nonExistentServices is null;

    var nonExistentServicesRepresentation = isNull
        ? "<null>"
        : nonExistentServices.ToString()
        ;

    // Result:
    // D8S.E0003.IService[]: IService instances
    // The returned enumerable for a non-existent service is not null.
    Console.WriteLine($"{nonExistentServicesRepresentation}: {nameof(IService)} instances");

    if (!isNull)
    {
        var nonExistenceServicesCount = nonExistentServices.Count();

        // Result:
        // Non existent services (Count: 0)
        //	Type: D8S.E0003.IService[]
        // The returned enumerable instance for a non-existent service is an empty array.
        Console.WriteLine($"Non existent services (Count: {nonExistenceServicesCount})\n\tType: {nonExistentServices.GetType().FullName}");
    }
}
    

The result of the fundamental service provider method IServiceProvider.GetService(typeof(IEnumerable<Type>)) is an empty array. Note that here, we are using the GetServices<T>() extension method for clarity.

Request an IServiceProvider

The most intriguing special ability of an service provider is that it can provide itself upon request.


public void TryGetIServiceProvider()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	// Succeeds.
	var serviceProviderFromServiceProvider = serviceProvider.GetService<IServiceProvider>();
}
    

This is useful when a service wants to choose its service dependencies dynamically at run-time, instead of the usualy way of statically specifying dependencies at compile-time. For example, you might have an widget factory service that based on the type of widget it's needs to build, it asks for different services.

However, some people consider this a "code smell" and call it the "service locator pattern". Their reasoning is that it can be hard to determine if you have any gaps in your tree service dependencies; are you sure you have added ServiceZ all the way down the line when ServiceA depends on ServiceB, and ServiceB on ServiceC, and so on and so on? At least when all service dependencies are simply declared in source code (for example, in the constructor of the service implementation) you can immediately see what dependencies are required. But if services are dynamically requested from a service provider, you are forced to reason through your program logic just to determine what dependencies are required.

Still this is a useful ability to be aware of, and it's interesting the that service provider can provide itself.

Operations that cause an exception

Now let's look at the operations that don't succeed, and by "don't succeed", I mean "throws an exception".

Request a required, but non-existent service

You already know how to determine if a service does not exist (the result of GetService(Type) is null), and this fact is incorported into the extension method GetRequiredService<T>(). This method allows you to easily state that "yup, if this service doesn't exist, that's a problem."


public void TryGetRequiredNonExistentService()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	// Fails, results in: System.InvalidOperationException: 'No service for type 'D8S.E0003.IService' has been registered.'
	var nonExistentService = serviceProvider.GetRequiredService<IService>();
}
    

Request a ServiceProvider

You are able to request the service provider as an IServiceProvider, but can you request the service provider as its actual type ServiceProvider?


public void TryGetServiceProvider()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	// Fails, results in: System.InvalidOperationException: 'No service for type 'Microsoft.Extensions.DependencyInjection.ServiceProvider' has been registered.'
	var serviceProviderFromServiceProvider = serviceProvider.GetRequiredService<ServiceProvider>();
}
    

The answer is no, and for good reason. In addition to the IServiceProvider interface, the ServiceProvider class implements the IDisposable interface. The service provider is disposable because it takes responsibility for managing the lifetime of the service instances it provides, services which might themselves be disposable. So when the service provider is disposed, it takes care of disposing all of its disposable services.

If a service could request the service provider as a disposable version of itself, it might accidentally (or malevolently!) call the Dispose() method. This would wreak havoc with all the services in the service provider. Suddenly, all the other disposable services would believe they had already been disposed, and the service provider itself would no longer work. A real disaster!

To prevent this, the service provider will only provide itself via the IServiceProvider interface.

You might think that mayhem is still a possibility, by casting the returned IServiceProvider to a ServiceProvider, but even this is prevented. The actual type of the returned IServiceProvider instance is not ServiceProvider, but Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, and you can't actually cast to this type because you cannot even utter its name! The type is hidden internally in the Microsoft.Extensions.DependencyInjection assembly, so you cannot state its name in code as part of casting.


// Error CS0122  'ServiceProviderEngineScope' is inaccessible due to its protection level
var serviceProviderAsServiceProvider = (Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope)serviceProvider.GetService<IServiceProvider>();
    

There might be a complicated way to use reflection to call the dispose method... But at least the service provider is protected from all but the most determined griefers.

Request a ServiceCollection

If you can request the service provider itself, maybe you can request the service collection the service provider was built from?

One motivation is that the service provider interface provides no way of listing all its services, but the service collection is trivial to iterate over. So if you could access the service collection it would be very useful for surveying the services in a service provider.

However, no such luck!


public void TryGetServiceCollection()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	// Fails, results in: System.InvalidOperationException: 'No service for type 'Microsoft.Extensions.DependencyInjection.ServiceCollection' has been registered.'
	var services = serviceProvider.GetRequiredService<ServiceCollection>();
}
    

Again, this is good thing. A service provider is built from a set of services at one specific point in time, and if you had access to a mutable service collection through the service provider, you might think you could change the set of available services after the service provider has been built, adding or removing as you please. But think about it: for service instances that have already been generated, how would you handle the case where the services they depend on get removed? Should they be removed too? Should the program crash?

It's better to maintain the hard separation between the phases of adding and removing services, and then using those services, which is provided when the service provider is built from a service collection.

Still, it's especially useful to survey the contents of a service provider. So if you want the service collection to be available, you can explicitly add the service collection instance to the service collection itself. Just treat ServiceCollection just like any other service. After adding the service collection instance to the service collection, then the service provider will be able to provide the service collection as a service.

But, remember an empty service provider cannot, by itself, provide the service collection it was built from.

Request an IServiceCollection

For all the same reasons requesting a ServiceCollection from an empty service provider fails, requesting an IServiceCollection fails too:


public void TryGetIServiceCollection()
{
	using var serviceProvider = Instances.ServicesOperator.GetEmptyServiceProvider();

	// Fails, results in: System.InvalidOperationException: 'No service for type 'Microsoft.Extensions.DependencyInjection.IServiceCollection' has been registered.'
	var services = serviceProvider.GetRequiredService<IServiceCollection>();
}
    

Conclusion

I hope by now you see that a service provider, even an empty one, is capable of doing many things. It can:

  • Return null for non-existent services,
  • Return an empty array for an enumerable of non-existent services,
  • And return itself as an IServiceProvider.

And by examining what an empty service provider cannot do, you can more intuitively understand its design:

  • Cannot return null if a service is requested as a required service using GetRequiredService<T>(),
  • Cannot return itseslf as a disposable ServiceProvider,
  • Cannot return the ServiceCollection it was built from (not even as an IServiceCollection).

I hope you found this useful, and see you next time!