Clean Architecture
Frameworks & Drivers
The web, the database, and the framework — kept as details, not as the architecture.
Every layer so far has depended on an interface and never on the thing behind it: IRepository<T>, not EfRepository<T>; a use case, not the endpoint that calls it. Something still has to introduce the two sides to each other. That something is the outermost ring — ASP.NET Core, Entity Framework, FastEndpoints, Identity — and the one file that wires all of it together.
Composing the object graph
Program.cs is the API project’s entry point, and it is the only file in eShopOnWeb that is allowed to know about every layer at once:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();FastEndpoints is composed here so ApplicationCore never imports it.
builder.Services.ConfigureLocalDatabaseContexts(builder.Configuration);
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();Identity and EF stores are startup details — the same names must not appear inward of this file.
builder.Services.AddCustomServices(builder.Configuration);AddCustomServices is the remaining seam: abstractions meet their implementations once, at process start.
FastEndpoints, EF Core, ASP.NET Identity — none of these names appear anywhere in ApplicationCore. They are details this file assembles at startup, the same way a plug assembles a lamp to whatever socket happens to be on the wall.
Binding the abstraction to the detail
The line that matters most is buried inside AddCustomServices:
public static void AddCustomServices(this IServiceCollection services, ConfigurationManager configuration)
{
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));This is the only method that names IRepository and EfRepository together. The bind happens once, at startup.
services.Configure<CatalogSettings>(configuration);
var catalogSettings = configuration.Get<CatalogSettings>() ?? new CatalogSettings();
services.AddSingleton<IUriComposer>(new UriComposer(catalogSettings));UriComposer is constructed from config here so use cases can depend on IUriComposer without reading appsettings.
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
}
IRepository<T> was declared back in The Dependency Rule, and EfRepository<T> did its work in Interface Adapters, but the two had never been mentioned in the same sentence until now. This method is the one place that names both. OrderService asks for an IRepository<Order> and never learns that a SQL Server call answers it; that binding happens here, once, at startup.
Swap SQL Server for Postgres, or drop the whole PublicApi project for a console harness, and this is the file that changes. ApplicationCore would not need to be told.
That still leaves a question this file does not answer: if OrderService cannot reference EfRepository<T> directly, how does it ever get data back out of a call it made outward? That trick — calling out without a dependency pointing out — is Crossing Boundaries.