Using Scrutor for Dependency Injection in ASP.NET Core API Applications
What is Scrutor?
Scrutor is a NuGet package that extends the built-in Dependency Injection (DI) container of ASP.NET Core with assembly scanning and decoration capabilities. While ASP.NET Core provides a simple and effective DI container out of the box, it requires every service to be registered manually — one line of code per interface-to-implementation mapping. In large enterprise applications with dozens or hundreds of services, this manual registration becomes repetitive, error-prone, and difficult to maintain.
Scrutor solves this problem by allowing developers to scan assemblies at startup and automatically discover and register all concrete classes that implement specific interfaces. Instead of writing individual AddScoped, AddTransient, or AddSingleton lines for each service, Scrutor enables a single builder.Services.Scan() call that handles all registrations from one or more assemblies.
Scrutor vs. Built-in ASP.NET Core DI
| Aspect | Built-in ASP.NET Core DI | Scrutor |
|---|---|---|
| Registration | Manual, one line per service | Automatic via assembly scanning |
| Scalability | Becomes verbose as services grow | Scales effortlessly — new classes are auto-registered |
| Maintenance | Must update Program.cs for every new service | No changes needed when new services are added |
| Error Risk | Forgetting to register a service causes runtime errors | All implementations are discovered automatically |
| Convention-Based | No | Yes — supports naming conventions and filtering |
| Decoration Support | Not supported natively | Built-in Decorate<TInterface>() support |
Advantages of Using Scrutor
2. Convention-Based Registration: Register services based on naming conventions, base classes, or attributes.
3. Reduced Boilerplate: Eliminates repetitive
AddScoped / AddTransient lines from Program.cs.4. Zero-Touch Scalability: Adding a new Repository or DataAccess class requires no changes to the DI configuration.
5. Filtering: Selectively include or exclude types using
AddClasses() with predicates.
Solution Architecture Diagram
Figure 1: Core_API Solution Architecture — Scrutor scans DataAccess and Repository assemblies and auto-registers all implementations with their interfaces in the DI container. Diagram by Mahesh Sabnis.
List of Solution Projects and Their Classes
The Core_API solution follows a clean layered architecture with 9 projects separated by responsibility. Below is an explanation of each project.
1. Com.Application.Domain.Entities
Contains the domain entity classes and the shared response model used across all layers.
| Class | Description |
|---|---|
| BaseEntity | Abstract base class that all entity classes inherit from. Provides a common type constraint for generic interfaces. |
| Department | Represents the Department table with properties: Deptno, Deptname, Location, Capacity. |
| Employee | Represents the Employee table with properties: Empno, Empname, Designation, Salary, Deptno, Ratings. |
| ResponseObject<TEntity> | Generic wrapper that carries a collection of records, a single record, a message, and a response code back to the caller. |
public abstract class BaseEntity { }
public partial class Department : BaseEntity
{
public int Deptno { get; set; }
public string Deptname { get; set; }
public string Location { get; set; }
public int Capacity { get; set; }
}
public class ResponseObject<TEntity> where TEntity : BaseEntity
{
public IEnumerable<TEntity>? Records { get; set; }
public TEntity? Record { get; set; }
public string? Message { get; set; }
public int ResponseCode { get; set; }
}
2. Com.Application.Domain.DataAccessContract
Defines the generic interfaces for data access operations. These interfaces abstract the database layer.
| Interface | Description |
|---|---|
| IReadDataAccess<TEntity, TPk> | Declares ReadAsync() and ReadAsync(TPk id) methods for reading data from the database. |
| IWriteDataAccess<TEntity, TPk> | Declares AddAsync(), UpdateAsync(), and DeleteAsync() methods for writing data to the database. |
public interface IReadDataAccess<TEntity, in TPk> where TEntity : BaseEntity
{
Task<ResponseObject<TEntity>> ReadAsync();
Task<ResponseObject<TEntity>> ReadAsync(TPk id);
}
public interface IWriteDataAccess<TEntity, in TPk> where TEntity : BaseEntity
{
Task<ResponseObject<TEntity>> AddAsync(TEntity entity);
Task<ResponseObject<TEntity>> UpdateAsync(TEntity entity);
Task<ResponseObject<TEntity>> DeleteAsync(TPk id);
}
3. Com.Application.Domain.Contract
Defines the generic interfaces for the repository (business) layer. Controllers depend on these interfaces.
| Interface | Description |
|---|---|
| IReadContract<TEntity, TPk> | Declares GetAsync() and GetAsync(TPk id) methods for reading operations at the business layer. |
| IWriteContract<TEntity, TPk> | Declares CreateAsync(), UpdateAsync(), and DeleteAsync() methods for write operations at the business layer. |
public interface IReadContract<TEntity, in TPk> where TEntity : BaseEntity
{
Task<ResponseObject<TEntity>> GetAsync();
Task<ResponseObject<TEntity>> GetAsync(TPk id);
}
public interface IWriteContract<TEntity, in TPk> where TEntity : BaseEntity
{
Task<ResponseObject<TEntity>> CreateAsync(TEntity entity);
Task<ResponseObject<TEntity>> UpdateAsync(TEntity entity);
Task<ResponseObject<TEntity>> DeleteAsync(TPk id);
}
4. Com.Applicaiton.Domain.ReadDataAccess
Contains concrete implementations of IReadDataAccess that use Dapper with PostgreSQL for read operations.
| Class | Description |
|---|---|
| DepartmentReadDataAccess | Implements IReadDataAccess<Department, int>. Executes SQL queries using Dapper to read Department records. |
| EmployeeReadDataAccess | Implements IReadDataAccess<Employee, int>. Executes SQL queries using Dapper to read Employee records. |
5. Com.Applicatoin.Domain.WriteDataAccess
Contains concrete implementations of IWriteDataAccess that use Entity Framework Core with PostgreSQL for write operations.
| Class | Description |
|---|---|
| DepartmentWriteDataAccess | Implements IWriteDataAccess<Department, int>. Uses EF Core DbContext for Add, Update, and Delete operations on Departments. |
| EmployeeWriteDataAccess | Implements IWriteDataAccess<Employee, int>. Uses EF Core DbContext for Add, Update, and Delete operations on Employees. |
| ApplicationDataDbContext | The EF Core DbContext class configured for the PostgreSQL database. Registered separately via AddDbContext. |
6. Com.Application.Domain.ReadRepository
Contains repository classes that implement IReadContract. Each repository receives its corresponding IReadDataAccess via constructor injection.
| Class | Description |
|---|---|
| DepartmentReadRepository | Implements IReadContract<Department, int>. Injects IReadDataAccess<Department, int> and delegates data retrieval to it. |
| EmployeeReadRepository | Implements IReadContract<Employee, int>. Injects IReadDataAccess<Employee, int> and delegates data retrieval to it. |
public class DepartmentReadRepository : IReadContract<Department, int>
{
IReadDataAccess<Department, int> dataAccess;
// IReadDataAccess<Department, int> is resolved by the DI container
// Scrutor auto-registered DepartmentReadDataAccess for this interface
public DepartmentReadRepository(IReadDataAccess<Department, int> dataAccess)
{
this.dataAccess = dataAccess;
}
async Task<ResponseObject<Department>> IReadContract<Department, int>.GetAsync()
{
return await dataAccess.ReadAsync();
}
}
7. Com.Application.Domain.WriteRepository
Contains repository classes that implement IWriteContract. Each repository receives its corresponding IWriteDataAccess via constructor injection.
| Class | Description |
|---|---|
| DepartmentWriteRepository | Implements IWriteContract<Department, int>. Injects IWriteDataAccess<Department, int> and delegates Create, Update, Delete operations to it. |
| EmployeeWriteRepository | Implements IWriteContract<Employee, int>. Injects IWriteDataAccess<Employee, int> and delegates Create, Update, Delete operations to it. |
8. Com.Application.Domain.ReadAPI
The ASP.NET Core Web API project for read operations. Exposes HTTP GET endpoints for Department and Employee data.
| Class | Description |
|---|---|
| DepartmentReadController | API Controller that injects IReadContract<Department, int> and exposes GET endpoints for Departments. |
| EmployeeReadController | API Controller that injects IReadContract<Employee, int> and exposes GET endpoints for Employees. |
| Program.cs | Configures Scrutor assembly scanning for automatic DI registration of all DataAccess and Repository services. |
9. Com.Application.Domain.WriteAPI
The ASP.NET Core Web API project for write operations. Exposes HTTP POST, PUT, and DELETE endpoints for Department and Employee data.
| Class | Description |
|---|---|
| DepartmentWriteController | API Controller that injects IWriteContract<Department, int> and exposes POST, PUT, DELETE endpoints for Departments. |
| EmployeeWriteController | API Controller that injects IWriteContract<Employee, int> and exposes POST, PUT, DELETE endpoints for Employees. |
| Program.cs | Configures DbContext registration and Scrutor assembly scanning for automatic DI registration of all DataAccess and Repository services. |
Using Program.cs for Configuring Scrutor for Dependency Injection
The core of Scrutor integration happens in the Program.cs file of each API project. Instead of writing individual AddScoped lines for every service, we use builder.Services.Scan() to automatically discover and register all implementations.
Before Scrutor : Manual Registration (Old Approach)
Previously, every interface-to-implementation mapping had to be registered explicitly:
// Manual registration — one line per service
builder.Services.AddScoped<IReadDataAccess<Department, int>, DepartmentReadDataAccess>();
builder.Services.AddScoped<IReadDataAccess<Employee, int>, EmployeeReadDataAccess>();
builder.Services.AddScoped<IReadContract<Department, int>, DepartmentReadRepository>();
builder.Services.AddScoped<IReadContract<Employee, int>, EmployeeReadRepository>();
// Problem: Adding a new entity (e.g., Product) requires adding
// TWO more lines here — easy to forget!
AddScoped lines in Program.cs. Forgetting even one registration leads to a runtime exception: "Unable to resolve service for type..."
After Scrutor : ReadAPI Program.cs
using Com.Applicaiton.Domain.ReadDataAccess;
using Com.Application.Domain.ReadRepository;
var builder = WebApplication.CreateBuilder(args);
// Use Scrutor to scan assemblies and automatically register
// DataAccess and Repository implementations with their interfaces
builder.Services.Scan(scan => scan
// Scan the ReadDataAccess assembly for DataAccess implementations
.FromAssemblyOf<DepartmentReadDataAccess>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime()
// Scan the ReadRepository assembly for Repository implementations
.FromAssemblyOf<DepartmentReadRepository>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime()
);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
After Scrutor : WriteAPI Program.cs
using Com.Application.Domain.WriteRepository;
using Com.Applicatoin.Domain.WriteDataAccess;
using Com.Applicatoin.Domain.WriteDataAccess.Models;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Register the DbContext — this is NOT handled by Scrutor
builder.Services.AddDbContext<ApplicationDataDbContext>(options => {
options.UseNpgsql(builder.Configuration.GetConnectionString("AppConn"));
});
// Use Scrutor to scan assemblies and automatically register
// DataAccess and Repository implementations with their interfaces
builder.Services.Scan(scan => scan
// Scan the WriteDataAccess assembly for DataAccess implementations
.FromAssemblyOf<DepartmentWriteDataAccess>()
.AddClasses(classes => classes.Where(
type => type != typeof(ApplicationDataDbContext)))
.AsImplementedInterfaces()
.WithScopedLifetime()
// Scan the WriteRepository assembly for Repository implementations
.FromAssemblyOf<DepartmentWriteRepository>()
.AddClasses()
.AsImplementedInterfaces()
.WithScopedLifetime()
);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
How Scrutor Scan() Works the Step by Step
FromAssemblyOf<T>(): Tells Scrutor which assembly to scan. The assembly is identified by providing any type that lives in that assembly. For example, FromAssemblyOf<DepartmentReadDataAccess>() targets the ReadDataAccess project assembly.Step 2 —
AddClasses(): Finds all concrete (non-abstract, non-interface) classes in the scanned assembly. An optional predicate can be passed to filter specific types, e.g., excluding ApplicationDataDbContext since the DbContext is already registered via AddDbContext.Step 3 —
AsImplementedInterfaces(): Maps each discovered class to the interfaces it implements. For example, DepartmentReadDataAccess is automatically mapped to IReadDataAccess<Department, int>.Step 4 —
WithScopedLifetime(): Registers all discovered services with a Scoped lifetime — meaning one instance is created per HTTP request. This matches the behavior of the original AddScoped calls.
AddDbContext<ApplicationDataDbContext>(), we must exclude it from Scrutor's scan using the AddClasses(classes => classes.Where(type => type != typeof(ApplicationDataDbContext))) filter to avoid duplicate or conflicting registrations.
Conclusion
Scrutor — Essential for Modern Complex Applications
As ASP.NET Core applications grow in size and complexity, the number of services, repositories, and data access classes can increase rapidly. In enterprise solutions with dozens of entities — each requiring its own DataAccess and Repository implementation — the built-in DI container demands a corresponding line of manual registration for every single service. This approach does not scale well and introduces a maintenance burden where a single missed registration can cause runtime failures.
Scrutor eliminates this problem entirely by bringing convention-based, assembly-scanning registration to ASP.NET Core's native DI container. With Scrutor:
- New services are automatically discovered — adding a new
ProductWriteDataAccessorOrderReadRepositoryclass requires zero changes to Program.cs. - The DI configuration stays clean and concise — a single
Scan()call replaces what could be dozens of individual registration lines. - Teams can work independently — developers can add new implementations in their respective projects without coordinating changes to the API startup configuration.
- Runtime errors from missing registrations are eliminated — if a class implements an interface, Scrutor registers it automatically.
For modern, complex enterprise applications that follow layered or clean architecture patterns with multiple assemblies, Scrutor is not just a convenience — it is a best practice that reduces human error, improves developer productivity, and ensures that the DI container always stays in sync with the codebase. Combined with ASP.NET Core's built-in DI container, Scrutor provides the assembly scanning power that was previously only available through third-party containers like Autofac or Unity, while keeping the simplicity and performance of the native container.
Code for this article can be downloaded from this link.
