Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close
    Deep Dive.NETPart 4 of 12

    Dependency Injection Setup Across Layers

    How to configure Dependency Injection in Clean Architecture across layers — registering services, repositories, and utilities for testable .NET apps.

    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.


    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.

    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.

    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.


    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.

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.