Onion Architecture in .NET showing domain application infrastructure and presentation layers
Back to Blog
Posted by Mahdi
Software Architecture

Onion Architecture in .NET: Layers and Libraries

Learn how Onion Architecture works in .NET, which layers and components to use, and how EF Core, MediatR, FluentValidation, Scrutor, specifications, mapping, messaging, and observability fit around the domain core.

Onion Architecture is a way to structure software so the business rules sit at the centre and every dependency points inward. In a .NET application, that usually means your Domain and Application projects do not depend on ASP.NET Core, Entity Framework Core, SQL Server, email providers, message brokers, cloud SDKs, or user-interface code. Those details live at the edge and plug into the core through interfaces, handlers, adapters, and dependency injection.

The value is not the diagram. The value is control. A team can test business behaviour without a database, replace infrastructure without rewriting the domain, keep framework code out of entities, and make architectural boundaries visible in the solution structure. Microsoft describes this family of approaches under Clean Architecture, noting that Hexagonal Architecture, Ports and Adapters, Onion Architecture, and Clean Architecture share a dependency-inversion idea: business logic and the application model sit at the centre while infrastructure depends on the core rather than the other way around.

For Australian businesses building long-lived .NET systems, Onion Architecture is useful when the domain logic matters: quoting, pricing, eligibility, booking, approvals, inventory, payments, compliance, case management, healthcare workflows, accounting rules, or anything else where the software should express business decisions rather than just move data between screens and tables. VaniTech can support this through cloud architecture, integration services, ongoing technical support, and custom .NET application design.

The Onion Architecture Idea in One Page

Keep the domain stable, push volatile technology outward, and make dependencies point toward the centre.

Domain at the Centre

Entities, value objects, aggregates, domain events, policies, and invariants live in the innermost layer.

Application Coordinates

Use cases, commands, queries, validators, DTOs, and transaction boundaries orchestrate the domain without owning infrastructure.

Infrastructure Implements

EF Core, repositories, email, file storage, queues, external APIs, clocks, identity providers, and cloud SDKs sit outside the core.

Presentation Adapts

ASP.NET Core controllers, Minimal APIs, Razor Pages, Blazor, workers, or mobile backends call the application layer.

DI Wires the Edge

The composition root connects interfaces to implementations at startup, usually in the Web or host project.

Tests Follow Boundaries

Unit test domain rules directly, test use cases with fakes, and integration test infrastructure adapters against real dependencies.

Onion Architecture layer map showing domain application infrastructure and presentation dependencies in a .NET solution
A practical Onion Architecture map for .NET applications: dependencies point inward, while infrastructure and presentation remain replaceable.

What Onion Architecture Means

Jeffrey Palermo introduced Onion Architecture as a response to traditional layered applications where business logic often depends directly on data access. In a classic three-layer .NET application, the UI depends on the business layer, and the business layer depends on the data-access layer. That is easy to draw, but it makes the business rules dependent on a database model, ORM behaviour, SQL schema, or external service code.

Onion Architecture turns that dependency around. The domain model is central. Around it sits application logic. Around that sit infrastructure and delivery mechanisms. Code in outer layers can reference inner layers. Code in inner layers should not reference outer layers.

That rule sounds simple, but it changes everyday design decisions. A domain entity should not call DbContext. A pricing policy should not know about an HTTP client. A use case should not construct a concrete SQL repository directly. A controller should not contain business rules just because it is close to the user request. If a business rule matters, it belongs closer to the centre.

Onion, Clean Architecture, Hexagonal Architecture, and Ports and Adapters

These architecture names are not identical, but they overlap enough that teams often use them together. Clean Architecture emphasises the dependency rule and independent business policy. Hexagonal Architecture and Ports and Adapters emphasise ports for inbound and outbound interactions. Onion Architecture emphasises concentric layers around the domain core. In a .NET team, the practical implementation is usually the same set of habits:

  • write domain behaviour in plain C#;
  • define abstractions near the code that needs them;
  • put database, messaging, file, email, identity, cache, and third-party API details in infrastructure;
  • wire concrete implementations in the host project;
  • protect the dependency direction with project references, tests, and code review.

The names matter less than the discipline. If the domain still depends on EF Core attributes, HTTP clients, static cloud SDK calls, and web request types, the solution may have Onion folders but not Onion Architecture.

Layer Model

Recommended .NET Projects and Components

A common .NET solution separates the core business model, application use cases, infrastructure adapters, host application, and tests.

Domain

Entities, value objects, aggregates, domain services, domain events, specifications, custom exceptions, and guard clauses.

Application

Commands, queries, handlers, interfaces, DTOs, validators, mapping profiles, pipeline behaviours, and use-case services.

Infrastructure

EF Core DbContext, repositories, migrations, email, storage, APIs, message buses, search, cache, identity, and telemetry exporters.

Presentation

ASP.NET Core Minimal APIs, controllers, Razor Pages, Blazor, background workers, admin tools, and API contracts.

Composition Root

Program.cs or the host startup code registers dependencies, options, authentication, middleware, logging, and infrastructure adapters.

Tests

Domain unit tests, application use-case tests, architecture tests, WebApplicationFactory tests, and real infrastructure integration tests.

The Inner Layer: Domain

The Domain project should be the quietest project in the solution. It should usually depend on the .NET base class library and perhaps a small number of carefully chosen packages that do not drag infrastructure into the model. Microsoft guidance for DDD-oriented .NET systems says the domain model should be plain C# and should not have hard dependencies on EF Core or another ORM.

Put the language of the business here. A Quote can expire. An Order can reject an invalid status transition. A Booking can check capacity. A Customer can change contact details through a method that enforces rules. A Policy can calculate eligibility. These behaviours are stronger than an anemic model full of public setters plus a service class that manipulates everything from the outside.

Typical Domain components include:

  • Entities: objects with identity and lifecycle.
  • Value objects: immutable concepts such as money, date ranges, email addresses, addresses, dimensions, and tax values.
  • Aggregates: consistency boundaries around related entities and value objects.
  • Aggregate roots: the entry point for changing an aggregate safely.
  • Domain services: domain logic that does not naturally belong to one entity.
  • Domain events: facts that occurred inside the domain, such as OrderSubmitted or InvoiceApproved.
  • Specifications: named reusable business or query criteria, when the complexity justifies them.
  • Custom exceptions and result types: ways to express invalid operations, failed rules, or recoverable outcomes.

Avoid putting these in Domain: EF Core DbContext, migrations, ASP.NET Core types, controller models, JSON attributes designed only for an API, SMTP clients, payment SDKs, queue clients, blob storage clients, logging sinks, or HTTP request objects. Some teams allow persistence-oriented attributes in simple systems, but once the domain is the asset, keeping it clean pays off.

The Application Layer: Use Cases and Orchestration

The Application project owns what the software can do. It coordinates tasks, calls domain methods, asks repositories or gateways for data, starts transactions, validates commands, maps between request models and domain concepts, and returns results. It should be thin in the DDD sense: it directs the domain, but it should not become a second home for business rules.

Application code often uses commands and queries. A command changes state: SubmitOrderCommand, ApproveTimesheetCommand, UpdateCustomerAddressCommand. A query reads data: GetInvoiceSummaryQuery, SearchProductsQuery, GetDashboardMetricsQuery. Whether you use MediatR or normal services, the same idea applies: each use case should be easy to find, test, and reason about.

Common Application components include:

  • command and query request models;
  • handlers or application services;
  • interfaces for repositories, unit-of-work abstractions, clocks, current-user access, email gateways, file storage, payment gateways, search, queues, and external APIs;
  • DTOs and response models used at application boundaries;
  • validators for request-level rules;
  • mapping definitions for DTOs and projections;
  • pipeline behaviours for validation, logging, performance timing, transactions, authorization checks, idempotency, and domain-event dispatching.

A good application layer does not know which database is used. It may know it needs IOrderRepository, IUnitOfWork, or IApplicationDbContext, but the concrete EF Core implementation belongs outside.

Recommended .NET library map for Onion Architecture showing MediatR FluentValidation EF Core Scrutor OpenTelemetry and Ardalis Specification
Dependency Direction

Reference Infrastructure Through Abstractions

The Web project can load Infrastructure at runtime, but Domain and Application stay independent. This keeps business logic testable and infrastructure replaceable.

The Infrastructure Layer: Details at the Edge

Infrastructure contains implementation details. In a .NET application this is commonly where EF Core lives: the DbContext, entity type configurations, migrations, repository implementations, SQL queries, and transaction handling. Microsoft's EF Core documentation describes a DbContext instance as designed for a single unit of work, with a short lifetime: create it, track entities, make changes, call SaveChanges or SaveChangesAsync, and dispose it. Microsoft also notes that DbContext is not thread-safe.

That guidance matters for Onion Architecture because a clean boundary does not remove the need to understand EF Core. It simply keeps EF Core in the right place. The Infrastructure project can use all of EF Core's strengths: change tracking, migrations, LINQ queries, compiled queries, interceptors, owned types, concurrency tokens, transactions, and provider-specific features. The Domain project does not need to know those mechanisms exist.

Infrastructure is also where you implement adapters for:

  • email providers such as SMTP, SendGrid, or Microsoft Graph;
  • file and object storage such as local disk, Azure Blob Storage, or S3-compatible storage;
  • payment providers and accounting systems;
  • message brokers such as Azure Service Bus, RabbitMQ, Amazon SQS, or SQL-backed queues;
  • search engines such as Azure AI Search, Elasticsearch, or OpenSearch;
  • caching such as Redis or in-memory caches;
  • identity integrations, directory services, and token validation;
  • external REST, GraphQL, SOAP, or gRPC services;
  • observability exporters and infrastructure-specific health checks.

The Presentation Layer: Delivery Mechanisms

The Presentation layer is whatever lets users, systems, or jobs reach the application. For .NET this may be ASP.NET Core Minimal APIs, MVC controllers, Razor Pages, Blazor Server, Blazor WebAssembly with a hosted API, gRPC services, Azure Functions, worker services, desktop apps, mobile backends, or admin jobs.

Keep presentation code focused on transport concerns: routing, authentication, authorization policies, request binding, response codes, serialization, API versioning, cookies, headers, model binding, file uploads, and UI-specific composition. When presentation code starts deciding whether an invoice may be approved or whether a customer is eligible for a discount, the onion is leaking.

The presentation project is often the composition root. It references Application and may reference Infrastructure so it can register concrete implementations in Program.cs. Microsoft's Clean Architecture guidance allows this practical reference, while cautioning that direct use of Infrastructure types should be limited to startup and dependency wiring.

Recommended .NET Library Choices

Onion Architecture does not require a specific package list. You can build it with only ASP.NET Core, EF Core, and Microsoft.Extensions.DependencyInjection. Libraries become helpful when they clarify use cases, remove repetitive plumbing, or enforce boundaries. They become harmful when every feature needs ceremony before it can express business value.

ConcernRecommended optionWhere it belongsWhy it helps
Runtime and framework.NET 10 LTS, ASP.NET CoreHost and presentationAs of 21 August 2026, .NET 10 is the current LTS line and is supported to November 2028 according to Microsoft's support policy.
PersistenceEntity Framework CoreInfrastructureUse DbContext, migrations, configurations, transactions, and provider-specific data access at the edge.
Use-case dispatchMediatR, or explicit application servicesApplication and host registrationMediatR provides in-process request, command, query, notification, and pipeline behaviour patterns. Use it when handler organisation helps the team.
ValidationFluentValidation core packageApplicationKeep request validation close to commands and queries. Prefer manual or explicit validation for async rules and Minimal API compatibility.
Query specificationsArdalis.SpecificationApplication or Domain, evaluated in InfrastructureEncapsulates reusable query logic and reduces custom repository method sprawl in larger systems.
Dependency registrationMicrosoft DI plus ScrutorComposition rootMicrosoft DI is enough for many apps. Scrutor adds assembly scanning and service decoration when conventions become useful.
MappingManual mapping, AutoMapper, or MapsterApplication or PresentationUse mapping for boundary DTOs and projections. Avoid hiding domain decisions inside mapping profiles.
MessagingMassTransit, Azure.Messaging.ServiceBus, or native broker clientsInfrastructure and workersUse for integration events, asynchronous workflows, outbox patterns, retries, and distributed communication.
ObservabilityMicrosoft.Extensions.Logging and OpenTelemetryHost and InfrastructureCollect logs, metrics, and traces without making domain code depend on vendor-specific monitoring tools.
TestingxUnit, NUnit, or MSTest; FluentAssertions if desired; Testcontainers where usefulTest projectsKeep domain tests fast, application tests focused, and infrastructure tests honest against real dependencies when practical.

MediatR in Onion Architecture

MediatR is popular in .NET Onion and Clean Architecture templates because it gives each use case a clear request and handler. Its README describes it as an in-process mediator supporting request/response, commands, queries, notifications, and events. In an Onion Architecture solution, MediatR usually lives in the Application layer, while registration happens in the host.

A typical command flow looks like this:

  1. The API receives POST /orders.
  2. The endpoint maps the request body to SubmitOrderCommand.
  3. MediatR sends the command to SubmitOrderCommandHandler.
  4. A validation pipeline runs a SubmitOrderCommandValidator.
  5. The handler loads an aggregate through an interface such as IOrderRepository.
  6. The aggregate enforces domain rules.
  7. The handler saves changes through a unit of work or application DbContext abstraction.
  8. Domain events are dispatched or queued through an outbox.
  9. The endpoint returns an HTTP response.

Use MediatR when it improves use-case clarity, cross-cutting behaviours, and testability. Skip it when a small application would be clearer with direct application services. The architecture is the dependency direction, not the mediator package.

FluentValidation in Onion Architecture

FluentValidation is useful for input validation: required fields, ranges, formats, conditional rules, cross-field checks, and async lookups. Its ASP.NET Core documentation now distinguishes manual validation from older automatic MVC pipeline validation, and warns that the ASP.NET validation pipeline approach is not recommended for new projects because it is synchronous, MVC-only, and harder to debug.

For Onion Architecture, prefer validators in the Application layer for command and query input. Keep domain invariants inside the Domain layer. For example, a validator can reject an empty email address before a command runs. The EmailAddress value object should still protect itself from invalid state. This avoids confusing request validation with business truth.

EF Core, Repositories, and Specifications

Repository debates in .NET are usually really boundary debates. EF Core's DbContext already implements repository and unit-of-work patterns in a broad sense, and Microsoft notes that directly using DbContext can be the simplest approach in simple CRUD scenarios. In more complex applications, custom repositories can still be useful because they encapsulate persistence and allow the Application or Domain layer to depend on abstractions rather than EF Core.

A pragmatic rule:

  • For simple CRUD screens, avoid a repository that only mirrors DbSet.
  • For aggregate persistence, use repositories when they express domain concepts such as GetOrderWithLines or GetActiveSubscription.
  • For complex reusable queries, consider specifications instead of adding endless methods to repository interfaces.
  • For read-heavy dashboards and reporting, allow query services or projections that are separate from aggregate repositories.

Ardalis.Specification is a common .NET choice for reusable query specifications. Its documentation describes the pattern as encapsulating query logic in its own class, promoting reuse, and helping prevent repository interfaces from growing with too many custom query methods. In Onion Architecture, the specification can be defined near the use case, while Infrastructure evaluates it with EF Core.

A Practical Solution Structure

A clear .NET solution might look like this:

src/
  Company.Product.Domain/
    Orders/
      Order.cs
      OrderLine.cs
      OrderSubmittedDomainEvent.cs
    Customers/
    Common/
      Entity.cs
      ValueObject.cs
      IAggregateRoot.cs
  Company.Product.Application/
    Orders/
      SubmitOrder/
        SubmitOrderCommand.cs
        SubmitOrderCommandHandler.cs
        SubmitOrderCommandValidator.cs
        SubmitOrderResult.cs
      GetOrder/
        GetOrderQuery.cs
        GetOrderQueryHandler.cs
    Abstractions/
      IApplicationDbContext.cs
      ICurrentUser.cs
      IClock.cs
      IEmailSender.cs
  Company.Product.Infrastructure/
    Persistence/
      AppDbContext.cs
      Configurations/
      Migrations/
      Repositories/
    Email/
    Storage/
    Messaging/
    DependencyInjection.cs
  Company.Product.Web/
    Program.cs
    Endpoints/
    Middleware/
    Contracts/
tests/
  Company.Product.Domain.Tests/
  Company.Product.Application.Tests/
  Company.Product.Infrastructure.Tests/
  Company.Product.Web.Tests/

This is only a starting point. Some teams merge Domain and Application into one ApplicationCore project, especially in smaller systems. Some teams split Presentation into Web.Api, Web.Admin, Workers, and Functions. Some teams organise Application by feature slices. That is fine. The important checks are:

  • Domain references no other project except shared kernel packages if truly needed.
  • Application references Domain, but not Infrastructure or Web.
  • Infrastructure references Application and Domain.
  • Web references Application and usually Infrastructure for composition.
  • Tests reference the layers they test.

What Goes Where in Real Features?

Feature itemRecommended layerReason
Order aggregateDomainOwns order state, lifecycle, and business invariants.
SubmitOrderCommandApplicationRepresents a use case, not a database row or HTTP request.
SubmitOrderCommandValidatorApplicationValidates request shape before domain work begins.
OrderSubmittedDomainEventDomainRecords a business fact that happened.
EfOrderRepositoryInfrastructureImplements persistence using EF Core.
AppDbContextInfrastructureDatabase session, mappings, migrations, and provider details.
POST /orders endpointPresentationHTTP-specific entry point that delegates to the Application layer.
EmailSenderInfrastructureWraps SMTP, Graph, or email provider SDK details.
CurrentUserPresentation or InfrastructureAdapts HTTP claims or host context to an application abstraction.
OutboxMessageInfrastructure, sometimes Application contractPersists integration events reliably without exposing broker details to Domain.

Component Patterns You Should Consider

1. Aggregate roots

Use aggregate roots to protect consistency boundaries. External code should change child entities through the aggregate root rather than editing everything directly. This is where many business invariants become obvious and testable.

2. Value objects

Use value objects for concepts where equality is based on values rather than identity. Money, EmailAddress, DateRange, Percentage, and Address are common examples. They reduce primitive obsession and keep rules close to the data they protect.

3. Domain events

Use domain events to express facts inside the business model. A domain event is not necessarily a broker message. It can later be translated into an integration event by the Application or Infrastructure layer.

4. Application handlers

Use handlers or application services for use cases. They should be orchestration points: load data, call domain behaviour, save changes, dispatch events, and return results. If handlers become long rule scripts, move behaviour inward.

5. Ports and adapters

Define ports as interfaces where the core needs something from the outside world. Implement adapters in Infrastructure. Examples include IEmailSender, IPaymentGateway, IFileStorage, ISearchIndex, IClock, and ICurrentUser.

6. Transaction and unit-of-work boundary

Most business commands need a clear transaction boundary. With EF Core, this is often SaveChangesAsync on one scoped DbContext. For distributed side effects, use an outbox rather than sending broker messages directly inside the same domain method.

7. Read models and projections

Do not force every read through aggregates if the read is only a dashboard, listing, export, or reporting view. Onion Architecture can work with CQRS-style separation: commands protect business invariants, while queries use efficient projections where appropriate.

8. Architecture tests

Add tests that prevent wrong project references and namespace dependencies. A simple architecture test can assert that Domain does not reference Infrastructure, Web, EF Core, or ASP.NET Core. This keeps the diagram honest.

Dependency Injection and the Composition Root

ASP.NET Core's built-in dependency injection is usually enough for Onion Architecture. Register Application services, validators, MediatR handlers if used, Infrastructure adapters, options, typed HTTP clients, DbContext, authentication, authorization, logging, and telemetry in the host startup. Keep the registration code readable by adding extension methods such as AddApplication() and AddInfrastructure(configuration).

Scrutor is useful when the team wants convention-based registration or decorators. Its README describes assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection. In Onion Architecture, decorators can be a clean way to apply cross-cutting behaviour such as caching, timing, retries, or logging around an interface without putting that behaviour inside the domain.

Use scanning carefully. It saves repetitive registration, but it can also hide what is registered. For smaller teams, explicit registrations are often easier to debug.

Mapping: Manual, AutoMapper, or Mapster?

Mapping is a boundary concern. API contracts are not domain entities. Database models are not always domain models. External provider payloads are not application commands. Some mapping is healthy.

Manual mapping is often best for commands and domain construction because it is explicit. AutoMapper or Mapster can be useful for read DTOs, projections, and repetitive object-to-object mapping. AutoMapper's documentation describes it as a convention-based mapper and says mapping often occurs at boundaries between layers where concerns differ.

Avoid these mapping mistakes:

  • Do not let mapping profiles contain important business decisions.
  • Do not map directly from public API models into tracked EF entities for complex updates.
  • Do not hide security decisions, ownership checks, or pricing rules in mapping hooks.
  • Do not use mapping to avoid designing proper command models.

Observability Without Polluting the Core

Logging, metrics, and traces are essential, but they should not make the domain depend on a monitoring vendor. Microsoft explains that .NET provides logging, metrics, and tracing APIs such as ILogger, Meter, and ActivitySource, while OpenTelemetry collects and exports telemetry to APM systems. OpenTelemetry's .NET documentation lists traces, metrics, and logs as stable signals.

In practice:

  • use ILogger in Application and Infrastructure where it helps operations;
  • avoid noisy logging inside entities and value objects;
  • create Activities around use cases, external calls, and message processing;
  • record low-cardinality metrics for key workflows;
  • use correlation IDs across HTTP, queues, and background jobs;
  • keep exporter and vendor configuration in the host.

Security and Authorization Boundaries

Authentication belongs near Presentation: tokens, cookies, identity providers, claims, sessions, and request context. Authorization is split. Coarse-grained policies such as "user must be an administrator" often belong in Presentation or Application pipeline behaviours. Business authorization such as "this clinician can approve this case because they are assigned to this region and the case is in review" often belongs closer to Application or Domain policy.

Avoid passing HttpContext into the domain. Instead, adapt it to an interface such as ICurrentUser or to explicit command data. The domain should know the business actor or role when needed, not the web framework that carried the request.

Common Mistakes

  • Repository-for-every-table: This creates ceremony without adding a domain boundary. Repositories should usually align to aggregates or meaningful persistence needs.
  • Anemic domain model: If entities are only getters and setters while handlers hold all rules, the centre is weak.
  • Interfaces for everything: An interface with one implementation is not automatically useful. Create abstractions around volatility, boundaries, and test seams that matter.
  • Infrastructure leaking inward: EF Core attributes, SQL concerns, SDK models, and HTTP objects inside Domain are warning signs.
  • Overusing MediatR notifications as domain events: In-process notifications and durable integration events have different reliability guarantees.
  • Ignoring read performance: Not every read needs to rebuild an aggregate. Use projections for read-heavy screens.
  • Mock-heavy tests: If tests mainly verify method calls between abstractions, they may be testing plumbing instead of behaviour.
  • No architecture enforcement: Boundaries drift when project references and dependency rules are not checked.

When Onion Architecture Is a Good Fit

Use Onion Architecture when the business rules are likely to outlive frameworks, databases, vendors, and UI channels. It is especially useful for long-lived systems with domain complexity, compliance needs, multiple integrations, heavy testing requirements, or a roadmap that may add mobile apps, admin portals, background workers, APIs, and partner integrations over time.

Good fit signals include:

  • business rules are difficult and valuable;
  • domain behaviour needs fast unit tests;
  • infrastructure choices may change;
  • several delivery channels share the same core rules;
  • the system integrates with multiple external systems;
  • the team needs clear ownership of use cases and dependencies;
  • the application will live for years, not weeks.

When It Is Too Much

Onion Architecture can be overkill for a small brochure site, prototype, simple CRUD admin tool, or short-lived internal utility. If every simple feature requires creating a command, validator, handler, interface, repository, DTO, mapper, and test double, the architecture may slow the team without protecting anything important.

For simpler .NET applications, start with a modular monolith and clear folders. Extract projects and abstractions when real complexity appears. Microsoft's web architecture guidance makes a similar point for monolithic applications: many systems are deployed as a single unit, and internal layers can still provide maintainability without jumping straight to distributed architecture.

A Practical Implementation Roadmap

  1. Map the domain language. Identify core concepts, workflows, policies, and invariants before drawing projects.
  2. Choose the project structure. Start with Domain, Application, Infrastructure, Web, and Tests unless the system is too small to justify that split.
  3. Protect references. Make Domain independent. Make Application depend on Domain only. Keep Infrastructure outward.
  4. Implement one vertical use case. Build one real command or query from endpoint to persistence to test the boundary.
  5. Add validation and mapping deliberately. Put request validation in Application. Keep business invariants in Domain.
  6. Design persistence around aggregates. Use EF Core in Infrastructure. Avoid repository methods that merely duplicate DbSet.
  7. Plan events and side effects. Separate domain events from integration events. Use an outbox for durable external messages.
  8. Add observability at the edge. Log use cases, external calls, and failures without coupling the domain to monitoring vendors.
  9. Write architecture tests. Enforce dependency direction automatically.
  10. Review ceremony every sprint. Remove abstractions that are not protecting a boundary or reducing complexity.

Example Decision: Should Application Use IApplicationDbContext or Repositories?

Both can fit Onion Architecture. Use repositories when aggregate persistence should be expressed in domain language or when you want to hide EF Core from use cases. Use an IApplicationDbContext abstraction when use cases need flexible LINQ projections and the team accepts that Application has a persistence-shaped port. Use direct EF Core only in Infrastructure or in simpler architectures where you are not enforcing Onion boundaries.

A common compromise is:

  • commands use repositories for aggregate loading and saving;
  • queries use read services, specifications, or projections;
  • Infrastructure implements everything with EF Core;
  • Domain remains persistence ignorant.

Final Recommendation

Build Onion Architecture around the parts of the system that deserve protection. In .NET, the most useful baseline is Domain plus Application at the centre, EF Core and external systems in Infrastructure, ASP.NET Core at the edge, and dependency injection in the host. Use MediatR, FluentValidation, Ardalis.Specification, Scrutor, AutoMapper, MassTransit, and OpenTelemetry when they support that boundary. Do not use them as badges of architectural seriousness.

The test is simple: can you explain the business rules without mentioning SQL tables, controllers, message brokers, or cloud SDKs? Can you test those rules quickly? Can you replace an external provider without rewriting the core? If yes, the onion is doing its job.

Sources Checked

FAQs

Onion Architecture in .NET FAQs

Short answers for teams deciding how to structure a maintainable .NET application.

Next Step

Design a .NET Architecture That Can Grow

VaniTech can help map your domain, design clean .NET boundaries, modernise EF Core and integration layers, and create a practical roadmap for a maintainable application.