Skip to main content

Dependency Injection Setup Across Layers

Dependency Injection Setup Across Layers
Table of Contents

Program.cs is where service registration goes to rot. It starts at five lines, then every feature adds its repositories, validators, and settings bindings until nobody can tell which layer owns what. The Contact Management Application avoids that with one rule: each layer registers its own services through an extension method, and Program.cs only calls those methods. This post walks through that setup layer by layer, using the real code, including two spots where the real code deserves a warning label.

What you’ll learn
#

  • Organizing DI with one service-extension method per layer
  • What the API, Application, and Infrastructure layers each register, and why
  • How the injected services flow through a create-contact request end to end
  • Two gotchas in the API-layer setup worth knowing before you copy it
This post is part of the Clean Architecture series built around the Contact Management Application. Start with the series introduction for the project structure and the full list of articles.

1. Why Organize DI Across Layers?
#

Service extension methods split the Dependency Injection setup along layer boundaries. What that buys you:

  • Separation of concerns: each layer manages its own dependencies; a repository registration in the Application layer is a code smell you can see.

  • Maintainability: changes in one layer’s DI configuration do not touch the others.

  • Scalability: new dependencies land in their own layer without disturbing the overall setup.

  • Clean startup code: Program.cs stays a short list of high-level calls instead of a registration dump.


2. Organizing Dependency Injection with Service Extensions
#

2.1 Overview of the Layers
#

The DI setup for the Contact Management Application is distributed as follows:

  1. API Layer: Manages API-related services, controllers, and middleware.

  2. Application Layer: Contains business logic, services, and orchestrations.

  3. Infrastructure Layer: Manages repositories, database access, and external services.

Each layer contains an extension method for registering its dependencies. These methods are then invoked at startup, keeping the DI logic contained in the layer it belongs to.


3. Setting Up Service Extensions for Each Layer
#

3.1 API Layer: Service Registration
#

The API layer handles incoming HTTP requests and relies on the other layers for processing. Below is the DI configuration for the API layer:

using Contact.Api.Core.Authorization;
using Contact.Api.Core.Middleware;
using Contact.Application;
using Contact.Application.Interfaces;
using Contact.Infrastructure;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddInfrastrcutureServices(builder.Configuration);
builder.Services.AddApplicationServices(builder.Configuration);

var appSettings = builder.Configuration.GetSection("AppSettings").Get<Contact.Application.AppSettings>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = appSettings.Issuer,
            ValidAudience = appSettings.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(appSettings.Secret))
        };
    });
builder.Services.AddAuthorization(options =>
{
    // Dynamically add policies based on permissions
    var permissionService = builder.Services.BuildServiceProvider().GetRequiredService<IPermissionService>();
    var permissionMappings = permissionService.GetAllPageOperationMappingsAsync().Result;

    foreach (var mapping in permissionMappings)
    {
        var policyName = $"{mapping.PageName}.{mapping.OperationName}Policy";
        options.AddPolicy(policyName, policy =>
        {
            policy.Requirements.Add(new PermissionRequirement(policyName));
        });
    }
});
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

// rest of code

Key highlights:

  • Registers services from both Application and Infrastructure layers using their extension methods.

  • Configures JWT Authentication for the API endpoints.

  • Adds custom authorization policies dynamically from the permissions returned by IPermissionService.

Watch out: builder.Services.BuildServiceProvider() inside AddAuthorization is a known anti-pattern; it builds a second container, so any singletons it creates are duplicates of the real ones. It survives here because it runs once at startup to read policy names, but an IAuthorizationPolicyProvider that resolves policies lazily is the cleaner design.

Two smaller notes on this block. AddInfrastrcutureServices really is misspelled, in the repository too; I have kept it as-is so copied code matches. And .Result on the async permissions call is tolerable only because this executes once during startup, never per request.


3.2 Application Layer: Service Registration
#

The Application layer owns the business logic and orchestrates workflows. It registers services, validators, and utilities required for processing requests.

Code: Application Layer DI Setup

public static class ApplicationServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddHttpContextAccessor();

        // Register AutoMapper with profiles
        services.AddAutoMapper(cfg =>
        {
            cfg.AddProfile<UserMappingProfile>();
        }, typeof(UserMappingProfile).Assembly);

        // Register FluentValidation validators
        services.AddValidatorsFromAssemblyContaining<RegisterUserValidator>();

        // Application settings
        services.Configure<AppSettings>(configuration.GetSection("AppSettings"));

        // Register services
        services.AddScoped<IUserService, UserService>();
        services.AddScoped<IPermissionService, PermissionService>();
        services.AddScoped<IRolePermissionService, RolePermissionService>();
        services.AddScoped<IContactPersonService, ContactPersonService>();

        return services;
    }
}

Key highlights:

  • Registers AutoMapper profiles for DTO-to-entity mapping and back.

  • Adds FluentValidation validators that check incoming DTOs.

  • Registers application-level services like IUserService and IContactPersonService.


3.3 Infrastructure Layer: Service Registration
#

The Infrastructure layer talks to external systems: the database, email, and other APIs. It registers repositories and data-access helpers.

Code: Infrastructure Layer DI Setup

using Contact.Application.Interfaces;
using Contact.Domain.Entities;
using Contact.Domain.Interfaces;
using Contact.Infrastructure.ExternalServices;
using Contact.Infrastructure.Persistence;
using Contact.Infrastructure.Persistence.Helper;
using Contact.Infrastructure.Persistence.Repositories;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Contact.Infrastructure;

public static class InfrastructureServiceCollectionExtensions
{
    public static IServiceCollection AddInfrastrcutureServices(this IServiceCollection services, IConfiguration configuration)
    {
      
        services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
        services.Configure<SmtpSettings>(configuration.GetSection("SmtpSettings"));
       
        services.AddScoped<IDapperHelper, DapperHelper>();

        services.AddScoped<IUnitOfWork, UnitOfWork>();
        services.AddScoped<IPermissionRepository, PermissionRepository>();
        services.AddScoped<IActivityLogRepository, ActivityLogRepository>();
        services.AddScoped<IRoleRepository, RoleRepository>();
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IRolePermissionRepository, RolePermissionRepository>();
        services.AddScoped<IUserRoleRepository,UserRoleRepository>();
        services.AddScoped<IGenericRepository<ContactPerson>, ContactPersonRepository>();
        services.AddScoped<IContactPersonRepository, ContactPersonRepository>();

        services.AddScoped<IEmailService, EmailService>();
        return services;
    }
}

Key highlights:

  • Registers DapperHelper and UnitOfWork to manage database interactions.

  • Configures repositories for entities like ContactPerson and Role.

  • Registers external services like IEmailService.


4. How DI Flows Across Layers
#

Let’s trace the flow of DI in handling a create contact request.

The snippets in this section are simplified to make the flow readable. The actual repository uses a generic service and a Dapper-based generic repository, not an EF Core DbContext; the Dapper article shows the real data-access code.

4.1 API Layer
#

The ContactPersonController uses IContactPersonService, injected via the API layer’s DI setup:

using Contact.Api.Core.Attributes;
using Contact.Application.Interfaces;
using Contact.Application.UseCases.ContactPerson;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Contact.Api.Controllers;

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ContactPersonController : ControllerBase
{
    private readonly IContactPersonService _contactPersonService;

    public ContactPersonController(IContactPersonService contactPersonService)
    {
        _contactPersonService = contactPersonService;
    }
}
// rest of code

4.2 Application Layer
#

The ContactPersonService implements the business logic and talks to the repository interface it received through the constructor:

public class ContactPersonService : IContactPersonService
{
    private readonly IContactPersonRepository _contactPersonRepository;

    public ContactPersonService(IContactPersonRepository contactPersonRepository)
    {
        _contactPersonRepository = contactPersonRepository;
    }

    public async Task<ContactPersonResponse> Add(CreateContactPersonDto contactDto)
    {
        var contact = new ContactPerson
        {
            FirstName = contactDto.FirstName,
            LastName = contactDto.LastName,
            Email = contactDto.Email,
            Mobile = contactDto.Mobile
        };

        return new ContactPersonResponse(await _contactPersonRepository.AddAsync(contact));
    }
}

4.3 Infrastructure Layer
#

The ContactPersonRepository persists the data (again simplified; the repo’s version sits on the Dapper-based generic repository):

public class ContactPersonRepository : IContactPersonRepository
{
    private readonly ContactDbContext _dbContext;

    public ContactPersonRepository(ContactDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<ContactPerson> AddAsync(ContactPerson contact)
    {
        await _dbContext.Contacts.AddAsync(contact);
        await _dbContext.SaveChangesAsync();
        return contact;
    }
}

Controller asks for a service, service asks for a repository, repository asks for a data helper. No layer constructs its own dependencies, which is exactly what makes each one testable in isolation.


The bottom line
#

  • One extension method per layer makes ownership visible: a registration in the wrong file stands out in code review.
  • Program.cs stays two lines per layer no matter how many services the application grows.
  • BuildServiceProvider() inside startup configuration duplicates singletons; if you need runtime data for policies, prefer a custom IAuthorizationPolicyProvider.
  • Sync-over-async (.Result) is only defensible in one-shot startup code; never let it leak into request handling.

What’s next
#

Clone the Contact Management Application, open AddApplicationServices, and add a service of your own end to end: interface in Application, implementation, registration, injection into a controller. It is the fastest way to feel how the layering holds up. The Dapper and repository pattern article continues the series on the data-access side.

Related

Implementing AutoMapper for DTO Mapping with Audit Details

··6 mins
Early versions of the Contact Management Application had the same four lines at the end of every create and update method: set CreatedBy, set CreatedOn (or their Updated* twins), then save. Copy-pasted audit code gets forgotten just often enough to make the audit columns untrustworthy, and an audit column you can’t trust is worse than none. Moving that logic into AutoMapper profiles fixed it in one place.

Error Handling and Exception Management in the API

··6 mins
An unhandled exception in a default ASP.NET Core API gives the client an empty 500 and gives you a stack trace in a log nobody is watching. The only thing worse is the API that returns the stack trace to the caller. The Contact Management Application takes the middle path: one middleware catches everything, maps known exception types to sensible status codes, returns a predictable JSON shape, and logs the details server-side. This post walks through that implementation and where I would lean on the newer built-in alternatives instead.

Best Practices for Creating and Using DTOs in the API

··6 mins
Serializing domain entities straight to the client looks like a shortcut until the first schema change. Rename one property on the entity and every consumer of the API breaks with it, and you find out from their bug reports. DTOs are the cheap insurance against that: the entity can change shape freely because the contract the client sees is a separate class you control. This post covers the DTO conventions I use in the Contact Management Application and why each one exists.