Dependency Injection Setup Across Layers

Dependency Injection Setup Across Layers
Dependency Injection Setup Across Layers


Introduction

Dependency Injection (DI) is a key aspect of Clean Architecture, ensuring decoupling between layers while enabling seamless communication. In the Contact Management Application, the DI setup is structured into APIApplication, and Infrastructure layers, with each layer responsible for registering its own services and dependencies. This modular approach maintains separation of concerns and improves the system’s scalability and maintainability.

This article will walk you through the DI setup across the layers, using real code examples to explain how services are organized, registered, and utilized.

In this article, we will cover:

  1. How to organize Dependency Injection using service extension methods for each layer.
  2. How to structure service extensions for the API, Application, and Infrastructure layers.
  3. The benefits of this approach in Clean Architecture.

Part of the Series

This article is part of a series covering the Contact Management Application, a sample project demonstrating Clean Architecture principles. Other articles include:

  1. Clean Architecture: Introduction to the Project Structure
  2. Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
  3. Clean Architecture: Validating Inputs with FluentValidation
  4. Clean Architecture: Dependency Injection Setup Across Layers (You are here)
  5. Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC)
  6. Clean Architecture: Implementing Activity Logging with Custom Attributes
  7. Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions
  8. Clean Architecture: Using Dapper for Data Access and Repository Pattern
  9. Clean Architecture: Best Practices for Creating and Using DTOs in the API
  10. Clean Architecture: Error Handling and Exception Management in the API
  11. Clean Architecture: Dockerizing the .NET Core API and MS SQL Server
  12. Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts

1. Why Organize DI Across Layers?

Service extension methods provide a convenient way to organize your Dependency Injection (DI) setup by separating DI configuration for each layer. This ensures that:

  • Separation of Concerns: Each layer manages its own dependencies, ensuring modularity and clarity.
  • Maintainability: Changes in one layer’s DI configuration do not affect others, making the system easier to manage.
  • Scalability: New dependencies can be added to their respective layers without disrupting the overall setup.
  • Clean Startup Code: Centralized DI configurations in extension methods keep the Startup.cs or Program.cs file clean.

By structuring DI this way, you maintain clear separation of concerns and improve the maintainability of the system.


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 contexts, and external services.

Each layer contains an extension method for registering its dependencies. These methods are then invoked in the Startup.cs file, keeping the DI logic well-organized and contained.


3. Setting Up Service Extensions for Each Layer

3.1 API Layer: Service Registration

The API layer handles incoming HTTP requests and relies on 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 secure API endpoints.

• Adds custom authorization policies dynamically using permissions fetched from the IPermissionService.


3.2 Application Layer: Service Registration

The Application layer handles the core 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, enabling DTO-to-entity mapping and vice versa.

• Adds FluentValidation validators to validate incoming DTOs.

• Configures application-level services like IUserService and IContactPersonService.


3.3 Infrastructure Layer: Service Registration

The Infrastructure layer interacts with external systems like databases, email services, and other APIs. It manages repositories and database contexts.

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:

  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

2. Application Layer: The ContactPersonService implements business logic and interacts with repositories via the Application Layer’s DI configuration: (Please note, code in repository is little different as we have implemented Generic Service)

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));
    }
}

3. Infrastructure Layer: The ContactPersonRepository persists the data using a database context: (Please note, code in repository is little different as we have implemented 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;
    }
}

5. Benefits of Layered DI Setup

Using service extension methods to encapsulate DI logic offers several advantages:

  • Separation of Concerns: Each layer is responsible for managing its own services and dependencies.
  • Cleaner Startup File: The Startup.cs file becomes more maintainable, as it only contains high-level service registrations for each layer.
  • Modularity: Dependencies for each layer can be changed or extended without affecting the rest of the system.
  • Scalability: As your application grows, you can easily manage additional services in a layer-specific manner.

Conclusion

In this article, we’ve explored how to structure Dependency Injection in a Clean Architecture setup by using service extension methods. This approach keeps the DI logic well-organized and encapsulated in each layer, ensuring that the API, Application, and Infrastructure layers manage their own dependencies.

By maintaining a clean and modular approach to Dependency Injection, we ensure that the application remains scalable, maintainable, and easy to test.

For a more detailed look at the project, you can access the full source code on GitHub:

GitHub Repository: Contact Management Application


Discover more from Nitin Singh

Subscribe to get the latest posts sent to your email.

5 Responses

  1. December 3, 2024

    […] Clean Architecture: Dependency Injection Setup Across Layers […]

  2. December 3, 2024

    […] Clean Architecture: Dependency Injection Setup Across Layers […]

  3. December 5, 2024

    […] Clean Architecture: Dependency Injection Setup Across Layers […]

  4. December 16, 2024

    […] Clean Architecture: Dependency Injection Setup Across Layers […]

  5. December 16, 2024

    […] Clean Architecture: Dependency Injection Setup Across Layers […]

Leave a Reply