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
ConfigureServicesorProgram.cs
- ❌ Leads to bloated, unreadable startup code
- ✅ Encapsulate registrations into modular extension methods
2. Not understanding service lifetimes
- ❌ Registering a
Scopedservice into aSingletoncan cause memory leaks or incorrect state sharing - ✅ Understand the difference between
Transient,Scoped, andSingletonLifetimes 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.
![]()






