← All writing
.NET apps / Clean Architecture / Backend

Organizing .NET Applications Around Use Cases

How Culina and Elysion use ASP.NET Core, MediatR and explicit persistence boundaries within a single API.

· 2 min read

Give a business operation a place to live

Culina and Elysion use .NET 10 and ASP.NET Core for their backends. Each exposes a single API application, with internal projects separating business concepts, use cases, persistence and hosting.

In Culina, controllers delegate application behavior to MediatR handlers. Commands and queries organize write and read use cases. Elysion uses the same style for a broader operational domain involving orders, inventory, documents and tenant administration.

The benefit of that organization is a recognizable place for an operation. An HTTP endpoint introduces a request; an application handler coordinates the work; persistence and integrations sit behind defined boundaries.

Read and write separation need not mean separate databases

These applications use CQRS-style command/query organization with Entity Framework Core and PostgreSQL. In Elysion, the separation retains a shared relational persistence model. Commands and queries describe responsibilities in application code, not a claim of event sourcing or independent read databases.

That distinction keeps the architecture description proportional to the implementation. A handler-oriented application can separate how information is changed from how it is retrieved without introducing a distributed system.

Keep external concerns behind useful interfaces

Culina supports local filesystem and Azure Blob Storage media implementations. Elysion has separate Persistence, Infrastructure and Storage projects, including document conversion through Gotenberg. These integrations have concrete lifecycles and failure modes that differ from the business records they serve.

Application interfaces give use cases a vocabulary for requesting those capabilities. Implementations contain provider-specific work. FluentValidation checks input, while AutoMapper supports mapping between representations; neither replaces the business decisions that belong in domain behavior or application use cases.

The HTTP contract remains its own responsibility

Both products expose versioned endpoints and OpenAPI contracts. Generated TypeScript clients connect the React frontends to those contracts. Internal refactoring and external contract evolution therefore need separate consideration: moving a handler does not inherently change the API, while changing a request or response can affect clients even if the handler remains in place.

Backend tests and integration coverage help exercise these boundaries. The structure gives those tests identifiable subjects, but the folder layout alone proves nothing about correctness. The useful result is an application in which a business operation, its dependencies and its externally visible behavior can be understood together.