IServiceCollection in C# - Simplified Beginner's Guide For Dependency Injection

Dependency injection is a useful technique for structuring code in a way that is testable, maintainable, and scalable. It involves passing instances of objects (or "dependencies") as arguments into other objects that need them, feeding them from the top down. We'll be looking at IServiceCollection in C# as a built-in solution that provides a convenient way to register and resolve dependencies. While I have historically preferred Autofac, I felt it necessary to come back and revisit the built-in mechanisms that we have access to.

Using a dependency injection framework offers several benefits, such as minimizing tight coupling between objects, simplifying unit testing, and promoting the Single Responsibility Principle. By using IServiceCollection to manage dependencies in your C# code, you can work towards more modular code that is easier to maintain and test in the long term.

Throughout this article, I'll explain dependency injection with IServiceCollection in C#. We'll cover the basics of IServiceCollection, dive into the core principles of dependency injection, explore some additional techniques, and more. By the end of this article, you should feel confident in your ability to utilize dependency injection to create scalable and maintainable software -- so let's get into it!


What's In This Article: IServiceCollection in C#

Remember to check out these other platforms:

// FIXME: social media icons coming back soon!


The Basics of IServiceCollection in C#

In dotnet, the IServiceCollection interface is used to register dependencies in a .NET Core application. It's a fundamental part of what we see when standing up ASP.NET Core applications in particular. If you've built an ASP.NET Core web application -- surprise, you were using this even if you didn't know it!

The IServiceCollection API is easy to use and simplifies dependency registration in a .NET Core application. When a service collection is created, it'll be used to register dependencies using methods such as:

  • AddSingleton: used to create a single instance of a dependency throughout the lifetime of an application
  • AddTransient: creates a new instance every time it's requested
  • AddScoped: creates a new instance per request within the scope.

Managing Dependencies with IServiceCollection in C#

One of the benefits of using IServiceCollection is the improved manageability of dependencies. It allows clear separation of concerns in registering services and it's much easier to maintain compared to hard-coding of dependencies. IServiceCollection in C# also promotes code reuse and helps with testing, making the testing process more efficient. When the IServiceCollection is used with other dependency injection libraries, the registration process can become even simpler.

Here's a simple code example of how to use IServiceCollection:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IMyService, MyService>();
    services.AddTransient<BookService>();
}

This example registers IMyservice as a singleton dependency, which means a single instance of the service is created throughout the lifetime of the application. BookService is registered as a transient dependency, which means that a new instance is created every time it's requested. Finally, the AddMvc() method is used to register the MVC framework -- which you may have seen before!


The Core Principles of Dependency Injection

Dependency injection is a design principle that follows two fundamental concepts, the Inversion of Control (IoC) and the Dependency Inversion Principle (DIP). The IoC describes how control is passed between classes in a program and the DIP describes the design principle of reducing class dependencies by introducing abstractions.

Single Responsibility Principle within Dependency Injection

The Single Responsibility Principle (SRP) is a core principle in object-oriented programming that describes the need for a class to have a single responsibility. It's useful to apply SRP in dependency injection because it helps ensure each class only has one reason to change, which speeds up development and reduces the potential for errors.

By isolating each class's responsibility, we can use dependency injection to decouple their dependencies, making the code easy to maintain and test. But if it's not obvious how Dependency Injection can immediately benefit here, think about the process of splitting up a class. Even if your methods are decoupled, you still need to go move stuff around into new classes, instantiate them in the right spot, pass them in through the constructors in the right spots, etc...

Dependency Injection can nearly trivialize the passing of dependencies into your classes. By wiring things up on your dependency injection container (IServiceCollection in C# for the scope of this article), the DI framework itself auto-resolves these for you. Bye-bye manual effort!

Open/Closed Principle within Dependency Injection

The Open/Closed Principle (OCP) states that classes should be open for extension but closed for modification. The idea is to design code that can be easily extended without modifying the original code's behavior. Applying the Open/Closed Principle allows us to create code that is much more flexible, maintainable, and testable using dependency injection. By creating code that adheres to the OCP, dependency injection can enable developers to substitute implementations without modifying the existing code -- and the less that changes, the less surface area for things exploding.

Tying These Together

Both the Single Responsibility Principle and the Open/Closed Principle are helpful when using dependency injection. By isolating a class's responsibility and designing code that can be extended without modifying its existing behavior, dependency injection simplifies development. This is because we're simultaneously promoting modularity, extensibility, and testability, improving code quality and maintainability. What's not to love about all of that?

An example of how these principles can be applied with dependency injection is by creating a class that depends on an interface instead of a concrete implementation. This way, when implementing that class, we can easily substitute the implementation with a new one that conforms to the same interface without updating the original class. If you're of the camp where that *immediately* feels like overkill (not every class needs an interface, of course), just consider at a minimum the situations where you have an external dependency that you may want to mock in your tests.


IServiceCollection in C# vs Other DI Containers

When it comes to dependency injection (DI), there are plenty of tools and libraries to choose from, each with its advantages and disadvantages. One of the most popular DI containers for .NET Core is the IServiceCollection, but how does it compare to other containers?

Alternative DI Containers to IServiceCollection in C#

One popular DI container is Autofac, my personal favorite, which offers extensive functionality, including instance and assembly scanning, that is not available out of the box with IServiceCollection. Autofac can handle more complex and customized scenarios in comparison to IServiceCollection, which has a simpler API. However, Autofac might be an overkill for small to medium-sized projects and requires more boilerplate. It's also important to note that IServiceCollection has evolved a great deal over the years, and as much as I love Autofac, I am sure the gap between features is rapidly closing.

Another popular DI container is Simple Injector, which focuses on performance and compile time verification. Its API is similar to that of IServiceCollection, but it has a more rigid model for registration and verification. Simple Injector is ideal for medium to large scale applications and can achieve high performance benchmarks but might become problematic when dealing with multiple dependencies. I have no professional experience working with this one, but I wanted to mention it.

Scrutor is also on the list of things to consider, so keep your eyes peeled!

Advantages of Using IServiceCollection in C# over Other DI Containers

Despite Autofac and Simple Injector's popularity, IServiceCollection is still the go-to choice for most .NET Core developers. One of the main benefits of using IServiceCollection is its integration with .NET Core, which allows DI registration in Startup.cs. Furthermore, IServiceCollection can be easily integrated with other dependency injection libraries, like Autofac or Simple Injector, if more complex scenarios arise. The fact that it's immediately accessible to us in ASP.NET Core without pulling in additional dependencies is a huge win. Here's a quick snippet to show you that yes, Autofac can be combined directly with IServiceCollection:

builder.Services.AddAutofac(); // allow autofac registrations in the container!
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddBlazorBootstrap();

Another significant advantage of IServiceCollection is its simplicity and flexibility. Its API is uncomplicated and easy to understand, making it ideal for small to medium-sized projects and ramping up with DI in general. Its flexibility allows you to add advanced features as needed, creating a smoother learning curve than other DI containers.

Overall, the choice between IServiceCollection and other DI containers primarily depends on a project's size, specific requirements, and your experience level in each of these. However, in most cases, IServiceCollection offers the flexibility required for most projects without sacrificing the essential features -- and that's coming from an Autofac fan!


Further Concepts with DI and IServiceCollection in C#

Dependency injection (DI) allows us as .NET developers to easily add new features and improve the flexibility of code. In this section, I'll introduce some slightly more advanced concepts with DI that can be applied by using IServiceCollection in C#.

How to Use Dependency Injection with Interfaces

The best practices for working with dependency injection suggest that interfaces should be used to reduce class dependencies and abstract the functionality of the code. By defining an interface, you can create a clear separation of concerns between your application's components, enabling DI to work more powerfully and efficiently.

Of course, there is a larger and larger audience of individuals who hate interfaces because they see them as bloat. The argument is that it's just an additional file, and the odds of an implementation being swapped for something else in actuality are slim to none in most cases. For me, I find adding interfaces close to zero overhead, and getting that bit of extra safety if I need to swap implementations feels great... Which for me, is almost every single time I want to go write coded tests across a class and not use real dependencies.

Best Practices for Creating Services with Dependency Injection

The best practices for creating services with DI revolve around designing code to be modular, scalable, and easy to understand. Abstractions should be utilized where possible to make code more flexible and less dependent on specific implementations. But again, based on points in the previous section, there's arguably a point where over-abstracting becomes ridiculous... so keep it in mind.

Service lifetime management is also important to ensure that your services operate efficiently and with minimal memory usage. So consider who needs to own and refer to what, and for what lifetime. I find in many situations things have dependencies for the entire lifetime of an application -- so I default to some patterns because of that. However, when things fall outside of that need, then I need to think a bit more carefully about how to maintain those lifetimes accordingly.

Practical Example of Advanced Techniques with Dependency Injection

Here's a simple code example of dependency injection that follows the best practices outlined above. Imagine we have a UserService class we want to register with DI. We can define an interface - IUserService - and create a new implementation of this interface in our UserService class.

Our project can utilize this interface to access the UserService functionalities easily. We also learned about the importance of lifetime management, so we will make use of AddTransient to add our IUserService implementation to IServiceCollection. In this situation, our (artificial) requirement is that we don't want the service reference to be re-used.

public interface IUserService
{
    void GetUserById(int id);
    void SaveUser(User user);
}

public class UserService : IUserService
{
    public void GetUserById(int id)
    {
        // ...
    }

    public void SaveUser(User user)
    {
        // ...
    }
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IUserService, UserService>();
}

This example follows the best practices of creating an interface for service definitions. We also considered lifetime management using AddTransient since we don't want the instance to be reused.


Wrapping Up The IServiceCollection in C#

Dependency injection with IServiceCollection in C# is a helpful tool you can leverage in your dotnet applications. Through the use of IServiceCollection in C# apps, you can quickly and efficiently create loosely coupled code that is easily maintainable and extendable.

We explored the basics of IServiceCollection and the core principles of dependency injection, such as Inversion of Control and the Dependency Inversion Principle. I also discussed the benefits of loosely coupled code, especially with respect to testable and maintainable software. We also discussed how to use IServiceCollection in C# with interfaces and best practices for creating services.

It's important to remember that while IServiceCollection is an excellent DI container, it's not the only option available. You should take the time to assess your needs and explore other DI containers (like Autofac!) before making a final decision. If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!