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

1. Assembly Scanning: Automatically discovers all classes in specified assemblies that implement interfaces.
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.

ClassDescription
BaseEntityAbstract base class that all entity classes inherit from. Provides a common type constraint for generic interfaces.
DepartmentRepresents the Department table with properties: Deptno, Deptname, Location, Capacity.
EmployeeRepresents 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.

InterfaceDescription
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.

InterfaceDescription
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.

ClassDescription
DepartmentReadDataAccessImplements IReadDataAccess<Department, int>. Executes SQL queries using Dapper to read Department records.
EmployeeReadDataAccessImplements 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.

ClassDescription
DepartmentWriteDataAccessImplements IWriteDataAccess<Department, int>. Uses EF Core DbContext for Add, Update, and Delete operations on Departments.
EmployeeWriteDataAccessImplements IWriteDataAccess<Employee, int>. Uses EF Core DbContext for Add, Update, and Delete operations on Employees.
ApplicationDataDbContextThe 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.

ClassDescription
DepartmentReadRepositoryImplements IReadContract<Department, int>. Injects IReadDataAccess<Department, int> and delegates data retrieval to it.
EmployeeReadRepositoryImplements 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.

ClassDescription
DepartmentWriteRepositoryImplements IWriteContract<Department, int>. Injects IWriteDataAccess<Department, int> and delegates Create, Update, Delete operations to it.
EmployeeWriteRepositoryImplements 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.

ClassDescription
DepartmentReadControllerAPI Controller that injects IReadContract<Department, int> and exposes GET endpoints for Departments.
EmployeeReadControllerAPI Controller that injects IReadContract<Employee, int> and exposes GET endpoints for Employees.
Program.csConfigures 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.

ClassDescription
DepartmentWriteControllerAPI Controller that injects IWriteContract<Department, int> and exposes POST, PUT, DELETE endpoints for Departments.
EmployeeWriteControllerAPI Controller that injects IWriteContract<Employee, int> and exposes POST, PUT, DELETE endpoints for Employees.
Program.csConfigures 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!
Problem with Manual Registration: Every time a new entity is added (e.g., Product, Order, Customer), developers must remember to add corresponding 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

Step 1 — 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.
Important Note for WriteAPI: The ApplicationDataDbContext(DbContext) class lives in the WriteDataAccess assembly. Since it is already registered via 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 ProductWriteDataAccess or OrderReadRepository class 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


Popular posts from this blog

ASP.NET Core 8: Creating Custom Authentication Handler for Authenticating Users for the Minimal APIs

ASP.NET Core 7: Using PostgreSQL to store Identity Information

Azure AI Document Intelligence: Processing an Invoice and Saving it in Azure SQL Server Database using Azure Functions