Some questions regarding Onion architecture

Solarion

Honorary Master
Joined
Nov 14, 2012
Messages
28,081
Reaction score
17,850
Figure-01-2.png

Hi guys. I am still unclear some some things regarding this type of project layout. I'll start for now with my EmailService. I currently have this in the Application layer. I have come across several people say an email sender is more Infrastructure which is fine.

But now this email service has a parameter called MailAttributes which contains the From, To, Sender, Body, Subject etc. Where do I put this class, in Domain or Application?

TIA.
 
Last edited:
It’s definitely not domain.

Not sure it’s application either, but I am not sure about the rules for these “over abstracted” architectures.

But I would say that the ability to send email is “infrastructure”.
 
It’s definitely not domain.

Not sure it’s application either, but I am not sure about the rules for these “over abstracted” architectures.

But I would say that the ability to send email is “infrastructure”.
Yes got nothing to do with your domain. Rather it’s a service that fits into/onto the infrastructure layer. Not quite as as ambient as logging.
 
Yes got nothing to do with your domain. Rather it’s a service that fits into/onto the infrastructure layer. Not quite as as ambient as logging.

And say a service which allows crud against the db, an employeeService for example, that is Infrastructure too?

I understand a couple of basic things. You have your models, your business logic and your db access. Those three are the more classic project layers I've used. In this case I would put anything that is a raw class into the Domain.
 
Last edited:
And say a service which allows crud against the db, an employeeService for example, that is Infrastructure too?

I understand a couple of basic things. You have your models, your business logic and your db access. Those three are the more classic project layers I've used. In this case I would put anything that is a raw class into the Domain.
It’s “application”.

Code:
//“Assign Offer To Member” feature
var offerType = _dbContext.OfferType.Find(offerTypeId);
var member = _dbContext.Members.Find(memberId);

var offer = member.assignOffer(offerType); //this is classic DDD where you are putting behavior on your domain object. In this case, assigning an offer (type) to a member, which might do some other things, and then returns the actual offer

dbContext.Offers.Add(offer);
dbContext.Save();

All the code above is in the “application” layer. The application is then using Data/Model + Domain
 
And say a service which allows crud against the db, an employeeService for example, that is Infrastructure too?

I understand a couple of basic things. You have your models, your business logic and your db access. Those three are the more classic project layers I've used. In this case I would put anything that is a raw class into the Domain.
no, employee crud is part of your domain because there are rules for employees in your domain.
Ui/BLL/DAL is so 1990’s.
The domain implements your business logic. I like to also have a whole set of Semantic Types that are specific to the domain. A lot of rules sit within these types and make programming your domain much easier and less bulky.

as to the original question, i tend to not make sending an email part of the main process flow because mail servers go down or time out. This interrupts your business process flow or makes it fail. I prefer to drop any notifications into some kind of outbound queue/table. Then a separate process trickles the notifications via mail server. This means that generating notifications is part of your domain and the actual sending not even part of your app. Convenient...
This also enables you to easily implement in-app notifications and/or email notifications because sometimes users hate emails and only want notifications when they are using the app.
 
I've been working with chatgpt to help me put together a project structure. The infrastructure layers I'm not 100% sure about despite it telling me this is popular. Another way it gave me was to just have an Infrastructure.Database project and create sub folders for both Ado and EF.

I have this project which implements an ADO.NET database layer with repositories and at some point I'm wanting to shift to EF hence the versioning.

I'm a little lost here and hoping for some feedback on this please. How do you guys deal with splitting up your concerns?

Between Jason Taylor's clean architecture and Chatgpt this is what I've come up with so far (The WebUI project is cut short in the diagram for brevity)

I think the Infrastructure.Common project can be dropped. Also atm this is an anemic domain. I'll get to that much later.

ProjectStructure.jpg
 
According to Chatgpt this is currently a popular design choice:

ProjectStructure2.jpg
 
Not a fan of “common”, “shared”, “utils”, “helpers” etc. they are kind of dumping grounds, but also sometimes organization is tricky.

Also not a fan of a folder called “Services”. Even in this example “ExternalService” is in “Services”, but “IdentityService” is in a folder called “Identity”

Back to your OQ.

Really depends on what “architecture” you are using.

As you may or may not know, I think most of these architectures make systems worse to work on, and more difficult to change, as they over abstract.

I have 5 basic projects in every solution.

Namespace.Api (bootstraps .Net webapi/mvc/razorpages - depends on all projects
Namespace.Features (Commands, Queries, Handlers, Validators, Mappers - depends on all projects except Api)
Namespace.Infrastructure (Anything that has nothing to with the application domain. Extension methods, HealthChecks, etc - no dependencies to other projects)
Namespace.Model (DbContext and associated objects. Migrations are here too - no solution dependencies other than it could depend on Infrastructure)
Namespace.External (Usually a bunch of HttpClient “wrappers” for providing finite integrating into external Api. Could also be things like an IFileClient with maybe a LocalFileClient or a S3FileClient. But this also gets tricky, where does something like a DistributedCacheClient live? Is it “infrastructure” or “external”? What i do here is to not sweat the small stuff, and build. Reorganize/refactor when/if it ever becomes a “problem” - no solution dependencies other than it could depend on Infrastructure)

As more things reveal themselves that we notice are used all the time, they are moved into other external nuget packages that get included in our .Net template.

This isn’t perfect, but it is simple (as in to understand and to change)


The ONLY reason that these folders/projects have meaning, is that we as a team/organization have agreed on what they mean.

I use a mix of “organize by feature” along with “organize by type”, heavily favoring “organize by feature”. An example of where I may favor “organize by type” is in Infrastructure/Extensions - here if might have LinqExtensions, StringExtensions, etc, because I am going to likely end up with a bunch of folders with 1 file, which is likely going to be tricky to name, and I don’t want to sweat the small stuff.
The above GPT example of the “Application” project folder layout is an abomination IMO :X3:

Forget about abstractions until they reveal themselves. Much easier to change a single file/folder/project into multiple once you understand the common problems you are encountering. Trying to guess these all up front is a recipe for a system that “looks impressive”, but is horrible to work on
 
Last edited:
@_kabal_ thank you for taking the time. Simpler is better. I like it. Thanks for the awesome feedback!
 
Ok so I see you use BadRequestException, NotFoundException. I used to also use that, but then argued with myself that this:
a) Forces you to categorise issues in my domain to match HTTP status codes, although useful in many cases, it's not a good idea
b) Something that is not found or fails validation (bad request) is not a situation where you should throw an exception. Exceptions are for situations that you did not anticipate. You can anticipate and code for something that is not found or has invalid values. Rather use Result<T> or ErrorOr<T>. This means you will code for something that is not found in your services/domain and then right at the end map it to a HTTP status code in the controller. But Result vs Exception that is also a matter of opinion and a whole rabbit-hole in itself

So the handler signature looks like this
C#:
public async Task<ErrorOr<ActivityCardModel>> Handle(Command command, CancellationToken cancellationToken)

and you can use several patterns to handle domain type errors:


C#:
        Activity activity = await dbContext.FindAsync<Activity>(command.ActivityId);
        if (activity == null)
          return Errors.NotFound<Activity>();

        Maybe<Error> maybeError = SomeValidator.Validate(user, activity);
        if (maybeError.HasValue)
          return maybeError.Value;

        ErrorOr<User> newAssignedToUser = await dbContext.FindOptionalEntityOrReturnNotFoundErrorAsync<User>(command.AssignedToUserId);
        if (newAssignedToUser.IsError)
          return newAssignedToUser.FirstError;

        Result updateNameResult = activity.UpdateName(command.Name);
        if (updateNameResult.IsFailure)
          return Errors.Validation(updateNameResult.Error);
 
I use this folder structure where:
  • Persistence.Sql is where the EFCore Fluent config sits. No annotations in domain objects because that bleeds storage concerns into the domain
  • Common is where all the common stuff like ErrorOr<T> Maybe<T>, Exceptions, etc sits
  • Domain is where the pure domain classes sit. This project has no dependencies other than Common & SemanticTypes. Not even persistence
  • SemanticTypes is where all the ValueObjects/SemanticTypes sit that is used throughout the app. A ubiquitous language of types
  • Shared holds all the WebApi DTOs, request types, response types etc. The reason why they are in their own project is so that an external party using C# can consume that assembly to have these types handy and not have access to the rest
  • WebApi is self explanatory
  • All the *.Test projects are the automated tests for all the classes in the corresponding project.
I will expand on the detail later (going into mtg)


1683112860977.png
 
ErrorOr is great because it is so simple to use with its implicit operator, and you are forced to handle the error state via Match/Switch (obviously you can bypass this by using .Value, but then you are just being silly on purpose)
 
Last edited:
ErrorOr is great because it is so simple to use with its implicit operator, and you are forced to handle the error state via Match/Switch (obviously you can bypass this by using .Value, but then you are just being silly on purpose)
Yep and actualy with the Maybe<T> and Either<A,B> as well
 
Yep and actualy with the Maybe<T> and Either<A,B> as well
Since “moving” to ErrorOr I have not really found an immediate need for Maybe/Option, especially when combined with non-nullable.

A “null” is just represented as an Error.NotFound
 
Top
Sign up to the MyBroadband newsletter
X