Some questions regarding Onion architecture

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
Yes I don't use Maybe often. If I do its Maybe<T>

I use Either<A,B> more in WebApi testing where my test helper method might return an error response or a DTO
 
To be honest, I am not a fan of the project structures listed above. I have adopted vertical slices with the mantra that "whatever changes together, lives together". Otherwise you find yourself moving around a lot in a large project. This is what my API folders look like
1683144993212.png
Each action is represented by an endpoint controller, MediatR query/command and Mediatr handler. All in one file. This is what the file looks like.
There is a whole lot of stuff I'd still like to change. e.g. Use ErrorOr<T> where I currently use Result<T>, but the basics are:
  • The request (and responses) are DTOs. They have no meaning in the domain and only use primitive types and public getters/setters
  • The MediatR command uses domain-aware ValueObjects so that I know that the handler gets sanitised / correct data. It also exactly matches the types that are used as propertied in the pure domain objects
  • The handler is the interface between persistence and the domain e.g. instantiates the domain object and saves using DbContext. The domain object has no dependency on any persistence concerns
I wanted to put the endpoint in the wrapping class as well but that broke ASP.NET. The class wrapping the command and handler merely assures that they are grouped together
So if the AddCity function breaks, I only need to come this this file. Not to a folder containing the Command and another containing the handler

C#:
[Route(CityRoutes.Cities)]
  [ScaffoldActionType(ScaffoldActionTypes.IsEntityAddAction)]
  public class AddCityEndpoint :
    EndpointBaseAsync.WithRequest<AddCityRequestDto>.WithResult<ActionResult>
  {
    public AddCityEndpoint(ILogger<AddCityEndpoint> logger, IMediator handler)
    {
      this.logger = logger;
      this.handler = handler;
    }

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    [Produces(MediaTypeNames.Application.Json)]
    [ApiExplorerSettings(GroupName = CityRoutes.GroupName)]
    public override async Task<ActionResult> HandleAsync([FromBody] AddCityRequestDto requestDto, CancellationToken cancellationToken = default)
    {
      if (requestDto == null)
        return BadRequest();

      Result<AddCity.Command> command = AddCity.Command.Create(requestDto);
      if (command.IsFailure)
        return BadRequest(command.Error);

      ErrorOr<long> response = await handler.Send(command.Value, cancellationToken);

      return CreatedAtOrErrorResult(CityRoutes.GetCityByIdRouteName, response);
    }

    private readonly ILogger<AddCityEndpoint> logger;
    private readonly IMediator handler;

  }

  public class AddCity
  {
    public class Command :
      ICommand<long>
    {
      public static Result<Command> Create(AddCityRequestDto requestDto)
      {
        Result<Name> name = Name.Create(requestDto.Name);
        if (name.IsFailure)
          return Result.Failure<Command>(name.Error);

        return new Command(name.Value, requestDto.ProvinceId, requestDto.CountryId, requestDto.Notes);
      }

      public Command(Name name, long? provinceId, long? countryId, string notes)
      {
        Name = name;
        ProvinceId = provinceId;
        CountryId = countryId;
        Notes = notes;
      }

      public Name Name { get; init; }
      public long? ProvinceId { get; init; }
      public long? CountryId { get; init; }
      public string Notes { get; init; }
    }

    public class Handler :
      ICommandHandler<Command, long>
    {
      public Handler(ILogger<Handler> logger, DbContext dbContext)
      {
        this.logger = logger;
        this.dbContext = dbContext;
      }

      public async Task<ErrorOr<long>> Handle(Command command, CancellationToken cancellationToken)
      {
        Province province = null;
        if (command.ProvinceId != null)
        {
          province = await dbContext.FindAsync<Province>(command.ProvinceId);
          if (province == null)
            return Errors.NotFound<Province>();
        }

        Country country = null;
        if (command.CountryId != null)
        {
          country = await dbContext.FindAsync<Country>(command.CountryId);
          if (country == null)
            return Errors.NotFound<Country>();
        }

        City city = City.Create(command.Name, province, country)
        {
          Notes = command.Notes
        };

        await dbContext.AddAsync(city, cancellationToken);

        await dbContext.SaveChangesAsync(cancellationToken);

        return entity.Id;
      }
    }
 
Last edited:
^^ it’s the way.

Although I am seriously triggered by the lack of curly braces for the 1 liner conditions and no “var”
:ROFL:
 
To be honest, I am not a fan of the project structures listed above. I have adopted vertical slices with the mantra that "whatever changes together, lives together". Otherwise you find yourself moving around a lot in a large project. This is what my API folders look like
View attachment 1518321
Each action is represented by an endpoint controller, MediatR query/command and Mediatr handler. All in one file. This is what the file looks like.
There is a whole lot of stuff I'd still like to change. e.g. Use ErrorOr<T> where I currently use Result<T>, but the basics are:
  • The request (and responses) are DTOs. They have no meaning in the domain and only use primitive types and public getters/setters
  • The MediatR command uses domain-aware ValueObjects so that I know that the handler gets sanitised / correct data. It also exactly matches the types that are used as propertied in the pure domain objects
  • The handler is the interface between persistence and the domain e.g. instantiates the domain object and saves using DbContext. The domain object has no dependency on any persistence concerns
I wanted to put the endpoint in the wrapping class as well but that broke ASP.NET. The class wrapping the command and handler merely assures that they are grouped together
So if the AddCity function breaks, I only need to come this this file. Not to a folder containing the Command and another containing the handler

C#:
[Route(CityRoutes.Cities)]
  [ScaffoldActionType(ScaffoldActionTypes.IsEntityAddAction)]
  public class AddCityEndpoint :
    EndpointBaseAsync.WithRequest<AddCityRequestDto>.WithResult<ActionResult>
  {
    public AddCityEndpoint(ILogger<AddCityEndpoint> logger, IMediator handler)
    {
      this.logger = logger;
      this.handler = handler;
    }

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    [Produces(MediaTypeNames.Application.Json)]
    [ApiExplorerSettings(GroupName = CityRoutes.GroupName)]
    public override async Task<ActionResult> HandleAsync([FromBody] AddCityRequestDto requestDto, CancellationToken cancellationToken = default)
    {
      if (requestDto == null)
        return BadRequest();

      Result<AddCity.Command> command = AddCity.Command.Create(requestDto);
      if (command.IsFailure)
        return BadRequest(command.Error);

      ErrorOr<long> response = await handler.Send(command.Value, cancellationToken);

      return CreatedAtOrErrorResult(CityRoutes.GetCityByIdRouteName, response);
    }

    private readonly ILogger<AddCityEndpoint> logger;
    private readonly IMediator handler;

  }

  public class AddCity
  {
    public class Command :
      ICommand<long>
    {
      public static Result<Command> Create(AddCityRequestDto requestDto)
      {
        Result<Name> name = Name.Create(requestDto.Name);
        if (name.IsFailure)
          return Result.Failure<Command>(name.Error);

        return new Command(name.Value, requestDto.ProvinceId, requestDto.CountryId, requestDto.Notes);
      }

      public Command(Name name, long? provinceId, long? countryId, string notes)
      {
        Name = name;
        ProvinceId = provinceId;
        CountryId = countryId;
        Notes = notes;
      }

      public Name Name { get; init; }
      public long? ProvinceId { get; init; }
      public long? CountryId { get; init; }
      public string Notes { get; init; }
    }

    public class Handler :
      ICommandHandler<Command, long>
    {
      public Handler(ILogger<Handler> logger, DbContext dbContext)
      {
        this.logger = logger;
        this.dbContext = dbContext;
      }

      public async Task<ErrorOr<long>> Handle(Command command, CancellationToken cancellationToken)
      {
        Province province = null;
        if (command.ProvinceId != null)
        {
          province = await dbContext.FindAsync<Province>(command.ProvinceId);
          if (province == null)
            return Errors.NotFound<Province>();
        }

        Country country = null;
        if (command.CountryId != null)
        {
          country = await dbContext.FindAsync<Country>(command.CountryId);
          if (country == null)
            return Errors.NotFound<Country>();
        }

        City city = City.Create(command.Name, province, country)
        {
          Notes = command.Notes
        };

        await dbContext.AddAsync(city, cancellationToken);

        await dbContext.SaveChangesAsync(cancellationToken);

        return entity.Id;
      }
    }


"return Errors.NotFound<Province>();"

what does this `NotFound` look like btw?

I want to add the same thing, and I am sure I can do it myself, but lazy ;)

I am usually doing this - return Error.NotFound("User.NotFound", "User not found.");
 
"return Errors.NotFound<Province>();"

what does this `NotFound` look like btw?

I want to add the same thing, and I am sure I can do it myself, but lazy ;)

I am usually doing this - return Error.NotFound("User.NotFound", "User not found.");

C#:
public class Errors
  {
    public static Error NotFound<TEntity>(string additionalInfo = null)
    {
      string message = additionalInfo == null
        ? $"{typeof(TEntity).Name} not found."
        : $"{typeof(TEntity).Name} not found: {additionalInfo}";

      return Error.NotFound(description: message);
    }

    public static Error Conflict<TEntity>(string additionalInfo = null)
    {
      string message = additionalInfo == null
        ? $"{typeof(TEntity).Name} has a conflict with the request."
        : $"{typeof(TEntity).Name} has a conflict with the request: {additionalInfo}";

      return Error.Conflict(description: message);
    }

    public static Error Validation(string additionalInfo = null)
    {
      return Error.Validation(description: additionalInfo);
    }

    public static Error Failure(string additionalInfo = null)
    {
      return Error.Failure(description: additionalInfo);
    }

    public static Error Forbidden(string additionalInfo = null)
    {
      return Error.Forbidden(description: additionalInfo);
    }
  }
 
C#:
public class Errors
  {
    public static Error NotFound<TEntity>(string additionalInfo = null)
    {
      string message = additionalInfo == null
        ? $"{typeof(TEntity).Name} not found."
        : $"{typeof(TEntity).Name} not found: {additionalInfo}";

      return Error.NotFound(description: message);
    }

    public static Error Conflict<TEntity>(string additionalInfo = null)
    {
      string message = additionalInfo == null
        ? $"{typeof(TEntity).Name} has a conflict with the request."
        : $"{typeof(TEntity).Name} has a conflict with the request: {additionalInfo}";

      return Error.Conflict(description: message);
    }

    public static Error Validation(string additionalInfo = null)
    {
      return Error.Validation(description: additionalInfo);
    }

    public static Error Failure(string additionalInfo = null)
    {
      return Error.Failure(description: additionalInfo);
    }

    public static Error Forbidden(string additionalInfo = null)
    {
      return Error.Forbidden(description: additionalInfo);
    }
  }

Cool, yeah, "code" is kind of pointless, specifically for NotFound.
 
Happy for a better suggestion for returning an error with some sort of description.

I know that would require work/constructive contribution though :p
I charge for my architectural services. ;)
 
^^ it’s the way.

Although I am seriously triggered by the lack of curly braces for the 1 liner conditions and no “var”
:ROFL:
Yes, {} for 1-liners are better, but my OCD for terseness gets the better of me. But I prefer explicit types over var. Just personal prefs/habit
 
Yep, I hate the use of var, wish it never became a thing.
Get better at naming things. Write better code :X3:

Java 7 days where terrible (at least it got a little better in Java 8)


Code:
List<Product> products = new ArrayList<Product>();

// wow, would have never known that the variable called products is a List of Product

Obviously that is a kind of facetious example. I kind of get it, when you have function results assigned to variables.

But it makes a huge difference for reading code, as your brain can just “filter” them out, and this is easier because the variables are then at the same indentation level.

I personally have never had an issue of an “incorrect” type being declared to what I expected.
Thankfully I don’t work in notepad either, so if you do ever have a situation where you might come up with a “bad” name, the IDE just shows you what it is - inline.

But as always, do what works best for the team, and probably what’s established in the community.
Don’t be that idiot who mixes them though :p
 
Last edited:
Get better at naming things. Write better code :X3:

Java 7 days where terrible (at least it got a little better in Java 8)


Code:
List<Product> products = new ArrayList<Product>();

// wow, would have never known that the variable called products is a List of Product

Obviously that is a kind of facetious example. I kind of get it, when you have function results assigned to variables.

But it makes a huge difference for reading code, as your brain can just “filter” them out, and this is easier because the variables are then at the same indentation level.

I personally have never had an issue of an “incorrect” type being declared to what I expected.
Thankfully I don’t work in notepad either, so if you do ever have a situation where you might come up with a “bad” name, the IDE just shows you what it is - inline.

But as always, do what works best for the team, and probably what’s established in the community.
Don’t be that idiot who mixes them though :p
Well my variable names are quite verbose so no issue there. Its just a pref. I concede that var is way less typing but with R# thats not much of an issue either.
 
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

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)


View attachment 1517925

This is a great layout. What happens if you have another project which shares much of the functionality say communication with Azure or the Domain functionality or you have 5 projects which do. What do you do then? At the moment I am focusing on two objectives : organization of common code within the organization and secondly within each project.
 
This is a great layout. What happens if you have another project which shares much of the functionality say communication with Azure or the Domain functionality or you have 5 projects which do. What do you do then? At the moment I am focusing on two objectives : organization of common code within the organization and secondly within each project.

Not 100% sure what you are asking.

When you say project, do you mean in the Solution sense, or some other entirely different "project?
 
Not 100% sure what you are asking.

When you say project, do you mean in the Solution sense, or some other entirely different "project?

An entirely different project. Our organization has several which are all quite different but it's a mess. 3 of them have an email service which is identical. A change on one means going into all 3 a d updating the code. All of them have a User, Product, Client entity. A change in one, have to go in and change them all. So I'm trying to basically create a centralized project, a type of company wide project which holds all of the shared stuff, and then use that in each project. Maybe a bad idea or I'm missing something. Possibly yours and Spacerat project structure has already given me the answer and I'm not seeing the forest for the trees.
 
An entirely different project. Our organization has several which are all quite different but it's a mess. 3 of them have an email service which is identical. A change on one means going into all 3 a d updating the code. All of them have a User, Product, Client entity. A change in one, have to go in and change them all. So I'm trying to basically create a centralized project, a type of company wide project which holds all of the shared stuff, and then use that in each project. Maybe a bad idea or I'm missing something. Possibly yours and Spacerat project structure has already given me the answer and I'm not seeing the forest for the trees.
We have NuGet packages that contain all our "base" technology.

So if you are always creating some custom wrapper to some service, just publish a nuget package (to a private/public repo), and reference it. how granular you go (number of nuget packages) is up to you.

e.g. all our apps need a way to turn the API's that they produce into a Typescript client, so we built this package - https://www.nuget.org/packages/RealmDigital.SourceGenerators.ApiClient.Typescript - so now we just create a console app, reference this library, and tell it where the input is and where the output should go, configure if you are using vue or react, do you want react-query or just axios client and bob's your uncle.
 
Top
Sign up to the MyBroadband newsletter
X