Clean Architecture
Interface Adapters
Where a controller, a presenter, and a database row are all translations of the same use case.
This layer holds the translators. On one side, HTTP requests and JSON responses; on the other, SQL rows and a database driver. Neither speaks the use case’s language directly, so something has to sit between them and convert. That something is a controller, a presenter, or a gateway — never the business rule itself.
The web side of the translation
CatalogItemGetByIdEndpoint answers GET api/catalog-items/{catalogItemId} in eShopOnWeb’s API:
public class CatalogItemGetByIdEndpoint(IRepository<CatalogItem> itemRepository, IUriComposer uriComposer)
: Endpoint<GetByIdCatalogItemRequest, Results<Ok<GetByIdCatalogItemResponse>, NotFound>>FastEndpoints is the delivery mechanism. The endpoint is a translator, not the catalog rule.
{
public override async Task<Results<Ok<GetByIdCatalogItemResponse>, NotFound>> ExecuteAsync(
GetByIdCatalogItemRequest request, CancellationToken ct)
{
var response = new GetByIdCatalogItemResponse(request.CorrelationId());
var item = await itemRepository.GetByIdAsync(request.CatalogItemId, ct);
if (item is null)
return TypedResults.NotFound();It still asks IRepository for an aggregate, then maps HTTP 404 — the entity never sees the status code.
response.CatalogItem = new CatalogItemDto
{
Id = item.Id,
CatalogBrandId = item.CatalogBrandId,
CatalogTypeId = item.CatalogTypeId,
Description = item.Description,
Name = item.Name,
PictureUri = uriComposer.ComposePicUri(item.PictureUri),
Price = item.Price
};CatalogItemDto is a public JSON shape. Guard.Against and private setters do not survive this trip.
return TypedResults.Ok(response);
}
}
It still depends on IRepository<CatalogItem>, the same interface Entities and The Dependency Rule already introduced. What is new is CatalogItemDto — a flat, public-setter class with exactly the fields a JSON response needs. It is not the CatalogItem entity. Nothing about Guard.Against, private setters, or UpdateDetails survives the trip; the entity’s invariants are its own business, not the API response’s.
The database side of the translation
The opposite direction happens in EfRepository<T>:
public class EfRepository<T> : RepositoryBase<T>, IReadRepository<T>, IRepository<T>
where T : class, IAggregateRoot
{
public EfRepository(CatalogContext dbContext) : base(dbContext)
{
}
}This is the first type in the chain that names CatalogContext. Swap the database and this seam changes, not OrderService.
Four lines, and this is the first place in the whole call chain that names CatalogContext, an Entity Framework DbContext. Everything inward of it — OrderService, CatalogItem, IRepository<T> — has been talking about aggregates the entire time. This class is the seam where those aggregates finally meet SQL Server, and it is also the only place that would need to change if the database moved to Postgres.
Two different technologies touch this layer — FastEndpoints on one edge, Entity Framework on the other — and neither one leaks past it. What is left, once you have named the endpoint and the repository, is the plumbing that makes each of them run at all: Frameworks & Drivers.