Contents
- 1. Introduction
- 2. Why is IServiceCollection Important?
- 3. Example for using IServiceCollection
- 1) Create a custom authentication service
- 2) Encapsulate the service registration of the infrastructure layer
- 3) Encapsulate the integration of third — party libraries
- 4) Create the service with options
- 5) Create the service with the environmental conditions
- 4. Questions
- 4. Conclusion
1. Introduction
IServiceCollection
is an interface defined in the Microsoft.Extensions.DependencyInjection
namespace. It serves as a collection of ServiceDescriptor
objects, each describing how a particular service should be resolved by the DI
container. And each ServiceDescriptor
includes:
- The service type (Type)
- The implementation type or instance (Type or object)
- The service’s lifetime (Singleton, Scoped, or Transient)
So this interface is primarily used during application startup to configure which services are available and how they should be instantiated.
We typically use the IServiceCollection
inside the ConfigureServices
of the Startup class (in .NET 5 and earlier) or in the Program.cs
file starting from .NET 6.
In .NET 5 and earlier:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<MyDbContext>();
services.AddTransient<IMyService, MyService>();
}
After .NET 6
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<MyDbContext>();
builder.Services.AddTransient<IMyService, MyService>();
2. Why is IServiceCollection
Important?
In both cases, services is an instance of IServiceCollection.
IServiceCollection
plays such a central role in ASP.NET Core, because all services are registered through a single point of entry. This makes it easy to manage dependencies and understand what services are available in your application.
By defining extension methods on IServiceCollection
, you can encapsulate feature-specific registrations into reusable modules. For example:
services.AddEmailModule();
services.AddPaymentGateway();
Each of these methods might internally register multiple services related to that module.
Most third-party libraries (like Entity Framework Core, Swagger, AutoMapper, etc.) provide extension methods on IServiceCollection
. This allows developers to easily integrate complex systems with minimal effort.
Example:
services.AddEntityFrameworkSqlServer();
services.AddSwaggerGen();
We can also conditionally register services based on the environment settings
if (env.IsDevelopment())
{
services.AddTransient<IDebugService, DebugService>();
}
This flexibility supports different behaviors across environments (development, staging, production).
IServiceCollection
integrates seamlessly with the IOptions<T>
pattern, which allows you to bind configuration values to strongly-typed objects and inject them into services:
services.Configure<PaymentOptions>(Configuration.GetSection("Payment"));
services.AddSingleton<IPaymentProcessor, PaymentProcessor>();
While ASP.NET Core ships with a built-in lightweight DI
container, you can replace it with more advanced containers like Autofac
or DryIoc
. These custom containers often accept an IServiceCollection
to import service registrations before switching over to their own internal mechanisms.
3. Example for using IServiceCollection
1) Create a custom authentication service
public static class AuthenticationExtensions
{
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key")),
ValidateIssuer = false,
ValidateAudience = false
};
});
return services;
}
}
and use as below
services.AddCustomAuthentication();
2) Encapsulate the service registration of the infrastructure layer
public static class InfrastructureRegistration
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
services.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<ISmsService, SmsService>();
return services;
}
}
and use as below
services.AddInfrastructure(Configuration);
3) Encapsulate the integration of third — party libraries
public static class MediatorExtensions
{
public static IServiceCollection AddMediatorHandlers(this IServiceCollection services)
{
//ingegrate the MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
return services;
}
}
and use as below
services.AddMediatorHandlers();
4) Create the service with options
public static IServiceCollection AddMyServiceWithConfig(this IServiceCollection services, Action<MyOptions> configure)
{
var options = new MyOptions();
configure(options);
services.AddSingleton(options);
services.AddTransient<IMyService, MyService>();
return services;
}
Use as below
services.AddMyServiceWithConfig(opt =>
{
opt.Timeout = TimeSpan.FromSeconds(30);
opt.MaxRetries = 3;
});
5) Create the service with the environmental conditions
public static IServiceCollection AddDevelopmentOnlyServices(this IServiceCollection services, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
services.AddTransient<IDebugService, DebugService>();
}
return services;
}
Use as below
services.AddDevelopmentOnlyServices(env);
4. Questions
Q: What are some common mistakes people make when using IServiceCollection
?
Answer:
- Putting all registration logic directly in
ConfigureServices
orProgram.cs
- ❌ Leads to bloated, unreadable startup code
- ✅ Encapsulate registrations into modular extension methods
2. Not understanding service lifetimes
- ❌ Registering a
Scoped
service into aSingleton
can cause memory leaks or incorrect state sharing - ✅ Understand the difference between
Transient
,Scoped
, andSingleton
Lifetimes and use them appropriately
3. Overusing AddSingleton<T>()
- ❌ Misuse of Singleton can lead to shared mutable state issues
- ✅ Only use Singleton for truly global, stateless services
4. Manually registering too many concrete types
- ❌ Writing many
services.AddTransient<...>
lines become hard to maintain - ✅ Use convention-based or reflection-based auto-registration instead
5. Ignoring registration order or conditional logic
- ❌ Later middleware or components may depend on unregistered services
- ✅ Be mindful of registration order, especially when integrating third-party libraries
6. Misusing IServiceProvider
to manually resolve services (Service Locator Anti-pattern)
- ❌ Manually calling
provider.GetService()
is an anti-pattern - ✅ Always rely on constructor injection instead
Q: How can you ensure your service registrations are as clean and scalable as possible?
Answer:
- We can encapsulate registration logic with extension methods
public static class ApplicationModuleExtensions
{
public static IServiceCollection AddApplicationCore(this IServiceCollection services)
{
services.AddScoped<IMediator, Mediator>();
services.AddTransient<IValidator<Order>, OrderValidator>();
return services;
}
}
Then call it below
services.AddApplicationCore()
.AddInfrastructure(Configuration)
.AddEmailServices();
This can keep the startup code clean and readable.
2. Organize registration by feature or layer
Group related services into modules:
services.AddDataAccess()
services.AddApplicationServices()
services.AddExternalIntegrations()
services.AddCustomMiddlewares()
This improves clarity and supports team collaboration.
3. Use configuration options (IOptions
) to parameterize services
services.Configure<PaymentSettings>(Configuration.GetSection("Payment"));
services.AddSingleton<IPaymentProcessor, PaymentProcessor>();
This makes your services more configurable and testable.
4. Avoid hardcoded dependencies
- ❌ Directly instantiating objects or using concrete types in registrations
- ✅ Favor interfaces over implementations to improve flexibility and testability
5. Automate registration with convention-based approaches
For example, use reflection to register multiple services at once:
var assembly = typeof(MyService).Assembly;
services.Scan(scan => scan
.FromAssemblies(assembly)
.AddClasses(classes => classes.InNamespaces("MyApp.Application.Services"))
.AsImplementedInterfaces()
.WithScopedLifetime());
6. Design for modularity with Feature Folders or Plugin architecture
- Each module handles its own service registration
- The main application simply composes these modules together
4. Conclusion
IServiceCollection
is the central mechanism for registering services in ASP.NET Core applications, enabling dependency injection throughout the application.
It allows developers to cleanly encapsulate service registrations using extension methods, promoting modular and reusable code.
When using IServiceCollection
, It’s important to understand service lifetimes (Transient, Scoped, Singleton) and to avoid anti-patterns such as registering concrete types unnecessarily or mismanaging dependencies.
It also supports conditional registration based on environment or configuration, making it flexible for different deployment scenarios.
Proper use of IServiceCollection
leads to maintainable, testable, and scalable applications.