Question Details

No question body available.

Tags

c# domain-driven-design onion-architecture

Answers (4)

December 31, 2025 Score: 5 Rep: 246 Quality: Low Completeness: 70%


  1. You have 2 layers of DTOs, Request and DTO itself, you only need HTTP request DTO and domain request/object. You transform HTTP DTO to domain request in application layer.

  2. Using Onion for CRUD applications is overkill, you need richer domain, without it you will have lots of boilerplate without
    much benefits

  3. Those simple validation rules could be a part of HTTP API contract. You could introduce validated types (ValidId, ValidName) to have Create method without those checks, but you would be only moving stuff around. Validation has to be somewhere.


January 1, 2026 Score: 4 Rep: 46,830 Quality: Medium Completeness: 100%

It's unfortunate that "Onion Architecture" uses the word "onion" because people focus on the layers, when the real focus is on loose coupling and the direction in which higher code depends on lower code.

Jeffery Palermo wrote an in-depth 3-part essay about Onion Architecture. In it he states "the main premise is that it controls coupling. The fundamental rule is that all code can depend on layers more central, but code cannot depend on layers further out from the core. In other words, all coupling is toward the center." Layers in Onion Architecture provide boundaries as designated points to implement loose coupling. Palermo never prescribed DTOs as the sole means of achieving loose coupling; quite the opposite, in fact.

In the opening paragraph of part 1, Palermo writes, "it emphasizes the use of interfaces for behavior contracts, and it forces the externalization of infrastructure." Straight away he sets expectations that loose coupling should be achieved using interfaces, not a Byzantine mapping between semantically meaningless data transfer objects.

I'm not saying DTOs should never be used in Onion Architecture — they have their place — but you should bear in mind what a data transfer object really is: a simple bag of data that gets serialized for transport across a network or in between processes. You don't have a network between layers, and if your application doesn't utilize threads, then a DTO just becomes an irritating chore to deal with; hence, your question.

In your example project, CreateIngredientRequest is a legit DTO; it represents a request to create an ingredient that was serialized as key-value pairs in the POST body of an HTTP request. And then things start going sideways from there.

The CreateIngredientDto class really could be an interface that CreateIngredientRequest implements. This adheres to Onion Architecture because the request resides in the infrastructure layer, which depends on the application service layer (hint: the application service layer should define the interface). This maintains the proper direction for dependencies that Palermo talks about, where outer layers depend on inner layers.

The reality is that the DTO and interface are likely to have the same number of lines of code, but you eliminate the incessant mapping if the HTTP request DTO implements the interface. Note that this might not scale well, because the serialization framework might get tripped up by the properties needed for the interface.

The DTOs in your project don't have any behavior; they are vapid containers for information passed from one layer to another — at best they become an elaborate form of parameter object requiring you to perform the same mapping all for the sake of "decoupling" and "best practices"! If anything makes me want to vomit, it's the blind adherence to "best practices", not mapping DTOs. Palermo addresses this when he says, "it emphasizes the use of interfaces for behavior contracts." You don't have any behavior, an anemic domain model, perhaps, but not behavior, so interfaces might not be as beneficial.

The truth is, loose coupling like this forces you to make trade-offs, which forces you to consider whether those trade-offs are worth it. I'd like to point out a couple of insightful comments from others on your post:

  • "This is a funny thing about demo projects - they are rarely supported for long, so YAGNI constatly fires and deprives them of any architecture." — Basilevs, in a comment on your question.
  • "just accept and return the Domain Model through all the layers" — Ewan, in a comment on this answer.

These two comments stab right to the heart of your question. First, a toy project lacks the complexity needed before Onion Architecture feels like it tows its own weight, and that's what Basilevs is saying; it's overkill — but I understand this is for learning purposes, so don't worry about this yet.

Second of all, this pain you feel now will only multiply in a larger project with more complexity; it's ok to compromise on some of the tenets of a particular architecture if it makes life easier provided it solves more problems than it introduces. As Ewan mentioned, passing a domain object down through multiple layers is an option; I've found this to be a good compromise between loose coupling and reducing boilerplate code.

Using domain models further up the dependency tree still feels Onion-ish because outer layers depend on inner layers when the domain model is defined in the core layer. The trade-off is the very outside layers can have a dependency on the core layer; this increases the blast radius of changes to the inner-most layer, but I've found the central layer is the most stable over time, as it represents core business rules that change less frequently.

I think you are right to question the utility of all those DTOs, because they aren't actually DTOs (they are more like parameter objects or an anemic domain model), the mapping code is an irritating chore, and your toy project isn't sufficiently complex enough to realize the benefits of Onion Architecture, but you're certainly doing a faceplant into all the drawbacks. Instead, focus on the essential elements of Onion Architecture: keep the direction of dependencies pointing inward, push infrastructure concerns to the shell, and take a more pragmatic approach to eliminate boilerplate code that causes more suffering than it prevents.


This really should be a separate question, but you asked if there was a better way to enforce business rules than by throwing exceptions. At some point you need to programmatically correct erroneous input, accept incorrect input and correct it later, or crash the application to prevent data from being corrupted. And yes, I consider x = 100 when the business said x shouldn't exceed 50 to be corrupted data.

December 31, 2025 Score: 3 Rep: 4,556 Quality: Medium Completeness: 50%

I would not vomit. All serious projects I worked on suffer from coupling of storage model, network format and domain model.

Specific problems are:

  • domain can not be updated without breaking storage or network backward compatibility
  • default values decided by domain affect serialization
  • storage format touch-ups lead to non-obvious logic errors on older or special data
  • network format mimics storage format and can not be integrated into third party systems
  • storage and network format versioning is next to impossible
  • removal of fields is impossible
  • etc. the pain is endless...

These have to be decoupled completely. Never couple these unless you plan to stop support in three months or less.

Do not use:

  • runtime annotations
  • reflection
  • ORMs (outside of DB DTO)
  • code generation

to bind layers. They couple formats and behaviors too.

I'm not sure about your other layers, but assume they may be affected by same issues.

January 2, 2026 Score: 1 Rep: 119,848 Quality: Medium Completeness: 60%

I get nervous whenever I see a name crossing with layers

FooRequest --> ApiController --> FooDto --> ApplicationService --> Foo --> Repository --> Dao --> DB FooResponse