Exploring ServiceCollection in .NET

Exploring ServiceCollection in .NET

Introduction

ServiceCollection is a dependency injection container class in .NET. In this post we will explore the ServiceCollection internals for a better understanding. This post assumes that you have a basic understanding of Dependency Injection. If you are not aware of dependency injection I would recommend you to go through some introductory posts for initial understanding and then come back here for the internal implementation in .NET.

I have an interest in going through the source code of microsoft dotnet and aspnetcore and explore the classes that implement certain features that are otherwise abstracted from us when using such features in our application.

I have written several posts on exploring the source code and understanding certain features. You can check those from the links below -

  1. Setup and debug microsoft aspnetcore source code
  2. Understanding CreateBuilder() method in AspNetCore (.NET 7)
  3. Exploring AddControllers() method in AspNetCore (.NET 7)

The above posts were written after the release of .NET 7 however it is applicable to any subsequent release of .NET. At the time of writing this post .NET 8 has recently been released and we would be taking a sneak peek into .NET 8 source code for the understanding of ServiceCollection.

First Interaction with ServiceCollection

Let us create a new API project in .NET8 using the below mentioned default settings -

  • Project Template - ASP.NET Core Web API
  • Framework - .NET 8.0(Long Term Support)
  • Authentication Type - None
  • Use Minimal API instead of Controllers

Let us see the code in Program.cs generated for us.

Program.cs

// Note: Some of the code which is not relevant for our discussion has been removed for brevity
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.Run();

The very first line creates and provide us a builder object to build and run our aspnetcore web app. I have explained the internals of CreateBuilder() method earlier.

If we navigate to the code of WebApplicationBuilder.cs we see that it provides a bunch of properties and the Build() method.

//
// Summary:
//     A builder for web applications and services.
public sealed class WebApplicationBuilder : IHostApplicationBuilder
{
    //
    // Summary:
    //     Provides information about the web hosting environment an application is running.
    public IWebHostEnvironment Environment { get; }
    //
    // Summary:
    //     A collection of services for the application to compose. This is useful for adding
    //     user provided or framework provided services.
    public IServiceCollection Services { get; }
    //
    // Summary:
    //     A collection of configuration providers for the application to compose. This
    //     is useful for adding new configuration sources and providers.
    public ConfigurationManager Configuration { get; }
    //
    // Summary:
    //     A collection of logging providers for the application to compose. This is useful
    //     for adding new logging providers.
    public ILoggingBuilder Logging { get; }
    //
    // Summary:
    //     Allows enabling metrics and directing their output.
    public IMetricsBuilder Metrics { get; }
    //
    // Summary:
    //     An Microsoft.AspNetCore.Hosting.IWebHostBuilder for configuring server specific
    //     properties, but not building. To build after configuration, call Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build.
    public ConfigureWebHostBuilder WebHost { get; }
    //
    // Summary:
    //     An Microsoft.Extensions.Hosting.IHostBuilder for configuring host specific properties,
    //     but not building. To build after configuration, call Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build.
    public ConfigureHostBuilder Host { get; }

    //
    // Summary:
    //     Builds the Microsoft.AspNetCore.Builder.WebApplication.
    //
    // Returns:
    //     A configured Microsoft.AspNetCore.Builder.WebApplication.
    public WebApplication Build();
}

The Services property is what is of our interest for this post. The Services property is of the type IServiceCollection and ServiceCollection class is an implementation of IServiceCollection interface.

The IServiceCollection interface is present in the Microsoft.Extensions.DependencyInjection.Abstractions project of the dotnet runtime repository. The ServiceCollection class implements the IServiceCollection interface and this class is also present in the same project as the IServiceCollection interface. These class are under the Microsoft.Extensions.DependencyInjection namespace.

Now that we have located both the IServiceCollection and ServiceCollection let us go through the source code of these next.

IServiceCollection Interface

namespace Microsoft.Extensions.DependencyInjection
{
    /// <summary>
    /// Specifies the contract for a collection of service descriptors.
    /// </summary>
    public interface IServiceCollection : IList<ServiceDescriptor>
    {
    }
}

As you can see that IServiceCollection is extending the Generic List interface with the list type as ServiceDescriptor. This is very important for our understanding. It means that the class implementing IServiceCollection has to implement all the method and properties of the IList interface which in turns inherits the ICollection, IEnumerable, IEnumerable.

Ok nice. We will see the implementation later in our post but for now atleast we can think of ServiceCollection as a List of ServiceDescriptor. The ServiceCollection is thus a container of all the services injected in our application. We can perform all sorts of operations on the builder.Services that we can perform on a List to manage the injected services for our application effectively.

ServiceCollection Class

It is now time to look into the ServiceCollection class and how it implements the IServiceCollection interface.

public class ServiceCollection : IServiceCollection
{
    private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
    private bool _isReadOnly;

    /// <inheritdoc />
    public int Count => _descriptors.Count;

    /// <inheritdoc />
    public bool IsReadOnly => _isReadOnly;

    /// <inheritdoc />
    public ServiceDescriptor this[int index]
    {
        get
        {
            return _descriptors[index];
        }
        set
        {
            CheckReadOnly();
            _descriptors[index] = value;
        }
    }

    /// <inheritdoc />
    public void Clear()
    {
        CheckReadOnly();
        _descriptors.Clear();
    }

    /// <inheritdoc />
    public bool Contains(ServiceDescriptor item)
    {
        return _descriptors.Contains(item);
    }

    /// <inheritdoc />
    public void CopyTo(ServiceDescriptor[] array, int arrayIndex)
    {
        _descriptors.CopyTo(array, arrayIndex);
    }

    /// <inheritdoc />
    public bool Remove(ServiceDescriptor item)
    {
        CheckReadOnly();
        return _descriptors.Remove(item);
    }

    /// <inheritdoc />
    public IEnumerator<ServiceDescriptor> GetEnumerator()
    {
        return _descriptors.GetEnumerator();
    }

    void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item)
    {
        CheckReadOnly();
        _descriptors.Add(item);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    /// <inheritdoc />
    public int IndexOf(ServiceDescriptor item)
    {
        return _descriptors.IndexOf(item);
    }

    /// <inheritdoc />
    public void Insert(int index, ServiceDescriptor item)
    {
        CheckReadOnly();
        _descriptors.Insert(index, item);
    }

    /// <inheritdoc />
    public void RemoveAt(int index)
    {
        CheckReadOnly();
        _descriptors.RemoveAt(index);
    }

    /// <summary>
    /// Makes this collection read-only.
    /// </summary>
    /// <remarks>
    /// After the collection is marked as read-only, any further attempt to modify it throws an <see cref="InvalidOperationException" />.
    /// </remarks>
    public void MakeReadOnly()
    {
        _isReadOnly = true;
    }

    private void CheckReadOnly()
    {
        if (_isReadOnly)
        {
            ThrowReadOnlyException();
        }
    }

    private static void ThrowReadOnlyException() =>
        throw new InvalidOperationException(SR.ServiceCollectionReadOnly);

    private string DebuggerToString()
    {
        string debugText = $"Count = {_descriptors.Count}";
        if (_isReadOnly)
        {
            debugText += $", IsReadOnly = true";
        }
        return debugText;
    }

    private sealed class ServiceCollectionDebugView
    {
        private readonly ServiceCollection _services;

        public ServiceCollectionDebugView(ServiceCollection services)
        {
            _services = services;
        }

        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public ServiceDescriptor[] Items
        {
            get
            {
                ServiceDescriptor[] items = new ServiceDescriptor[_services.Count];
                _services.CopyTo(items, 0);
                return items;
            }
        }
    }
}

The implementation starts with first declaring the List that would be managed with the ServiceCollection object.

private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
private bool _isReadOnly;
public int Count => _descriptors.Count;
public bool IsReadOnly => _isReadOnly;

The public properties Count and IsReadOnly is then declared. The Count property gives the count of the services injected in the container. The IsReadOnly property is used to prevent the modification of the injected services from outside if the property is set to true.

The injected services are made readonly by the method call to MakeReadOnly(). This method sets the IsReadOnly property to true.

public void MakeReadOnly()
{
    _isReadOnly = true;
}
private void CheckReadOnly()
{
    if (_isReadOnly)
    {
        ThrowReadOnlyException();
    }
}

Before performing any modification to the ServiceCollection a call to CheckReadOnly() is made to throw an exception when we try to modify the ServiceCollection which is set to readonly by MakeReadOnly().

The indexing capability is added to the ServiceCollection by the indexer mentioned below so that we can navigate through the ServiceCollection though an integer based index.

public ServiceDescriptor this[int index]
{
    get
    {
        return _descriptors[index];
    }
    set
    {
        CheckReadOnly();
        _descriptors[index] = value;
    }
}

The remaining code just plays around with modification of the list _descriptors which in turn modifies the ServiceCollection.

Thats all for now. I hope you might a got an insight of the things happening behind the scenes inside the ServiceCollection class. When I first started with aspnetcore I was puzzled to understand these basics and would just remember things based of the usage I have done in projects or samples. Having an in depth source code walkthrough gives us an understanding so that we do not need to remember things without knowing the internals.

Conclusion

In this post ,

  • We encountered the point of first interaction with the ServiceCollection class in a sample web api project.
  • We saw that the ServiceCollection class is a Dependency Injection container class in dotnet present in Microsoft.Extensions.DependencyInjection namespace.
  • ServiceCollection is an implementation of the IServiceCollection interface and both of them are present in Microsoft.Extensions.DependencyInjection.Abstractions library.
  • IServiceCollection interface extends the IList interface with the type of list represented as a ServiceDescriptor class.
  • ServiceCollection class internally maintains all the services injected to it in a list of ServiceDescriptor.

These are some of the basics of ServiceCollection class however .NET and AspNetCore has a great usage of the extensions method. The extension methods on top of ServiceCollection further adds more and more features to it.

Some of the important extension methods on ServiceCollection are mentioned below. These method are used to register services to the container with a specific lifetime.

  • AddScoped
  • AddSingleton
  • AddTransient

We will explore these extension methods in another post.

Thank you for reading the post and see you in the next post.