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
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:
API Layer: Manages API-related services, controllers, and middleware.
Application Layer: Contains business logic, services, and orchestrations.
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 codeKey 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.
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
IUserServiceandIContactPersonService.
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
ContactPersonandRole.Registers external services like
IEmailService.
4. How DI Flows Across Layers#
Let’s trace the flow of DI in handling a create contact request.
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 code4.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 customIAuthorizationPolicyProvider.- 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.
