Skip to content

A comparison of various approaches for implementing clean architecture in C#

Notifications You must be signed in to change notification settings

MrWolfZ/clean-architecture-comparison

Repository files navigation

Clean Architecture Comparison

This repository contains a comparison of multiple styles for implementing a clean architecture in .NET (using .NET 5 and ASP.NET Core).

Note that clean architecture is arguably just the latest of a long list of names (including hexagonal / ports and adapters and onion among others) that describe the same fundamental concept for defining your architecture: dependency inversion. Therefore this comparison is also somewhat applicable to those styles, although the naming of concepts slightly differs.

The scenario we are using for showcasing the different styles is a simple task list management application.

Our domain model has the following properties:

  • there are task lists that are owned by users
  • a task list has a name and contains task list entries
  • a task list entry has a description and can be marked as done
  • there are premium and non-premium users

The following are our application's business rules:

  • a task list's name must be unique per owner
  • non-premium users are limited to at most one task list with at most 5 entries

In addition to our business rules, our application has a few more behaviors:

  • the system gathers statistics about changes that are made to the task lists it manages
  • whenever a task list is created or changed the system publishes a change notification to other systems

For the sake of simplicity our system contains no user interface, but only APIs. It also does not have any authentication or authorization, i.e. users only exist as business objects.

Baseline

To be able to compare the different styles it is useful to have a baseline implementation of our application to compare against. This baseline is a simple implementation using basic architectural patterns and is something that most readers should be familiar with from textbooks or their studies.

Domain-Driven Design

While not strictly necessary for a clean architecture, domain-driven design (DDD) is a natural fit for the separation a clean architecture aims to achieve. Therefore, as a first step on our journey towards clean architecture we refactor the baseline to use concepts of domain-driven design (i.e. aggregates, entities, domain events etc.).

Core

Each compared style is a standalone application that has some common cross-cutting concerns (e.g. API documentation, testing infrastructure, base classes). To prevent having the code for these concerns duplicated in each application, we have a number of core projects that provide this common code.

Moving to a clean architecture

Every architectural change should have a clear driver and moving to a clean architecture is no different. Many applications would work just fine with a simple architecture. For our scenario the driver for this change is a new business requirement. The business wants to send out reminders to users that have task lists with pending items that have not changed for a long time. Let's phrase this as a complete requirement with some more details:

  • the system sends reminders to premium users that have task lists with at least one pending entry
  • any task lists that have not been changed in 7 days are included in the reminder
  • reminders are repeated every 7 days if still applicable

If we think about how to implement this requirement, one solution that comes to mind is a cron job that runs every hour and checks for task lists for which reminders should be sent. Looking at the structure of our baseline and DDD solution it is not fully clear how to add such a job to the project. One solution would be to add web APIs that implement the required behavior and then using a script or a small console app to call those APIs regularly (through some kind of job scheduler). This solution would work, but given that our system may be storing millions of task lists, running this kind of work load on the web server is not optimal. An alternative solution - and the one we will be exploring here - is to implement the new business logic in a dedicated console application that runs offline and is triggered periodically by a job scheduler. These kinds of console apps are perfect for potentially long running processes.

Now that we have settled on a solution design, we need to determine how to incorporate the new behavior into our application. In our baseline and DDD we had all logic (business rules, persistence, APIs) in a single web project. For our new job we should now create a new console application. The simplest way to do this would be to just reference the web project from the console project or to implement the required logic from scratch in the new project. However, the former pollutes the console app with web code it does not require and the latter reduces maintainability by introducing redundancy. This is where we can use clean architecture to provide a structure that allows sharing our domain logic, business rules, repositories etc. between a web app and the console app.

Below you can find a list of the various styles we are comparing for implementing the scenario outlined above. We recommend you to go through the list in order since some styles re-use concepts from other styles. We also encourage you to clone the repository and look at the code in your favorite IDE for easier navigation.

  • (work-in-progress) basic: in this style we simply split the DDD example into layers according to clean architecture (i.e. domain, application, infrastructure, jobs, and web)
  • (work-in-progress) command query separation (CQS): an extension of the basic style that models operations as commands and queries
  • (work-in-progress) mediator: a variant of CQS that uses the mediator pattern for handling commands, queries, and domain events (using the MediatR library)
  • (work-in-progress) decorator: a variant of CQS that uses the decorator pattern for handling cross-cutting concerns (e.g. logging and validation) of our command and query handlers
  • (work-in-progress) proxy: a variant of CQS that uses the proxy pattern for handling cross-cutting concerns of our command and query handlers (using the Castle library)
  • (work-in-progress) functional: a variant of CQS that uses concepts of functional programming instead of object-oriented programming (using the language-ext library)

Open Points

  • in basic (and derived) structure test project by use-cases instead of by layer
  • in cqs separate write and read repositories
  • in core add more behaviors
    • RetryOnException
    • FallbackResponse
    • Authorization
    • for queries CacheResponse
  • add README for ddd
  • add README for basic
  • adjust cqs based on basic
    • split write and read repositories
  • create cqs-proxy
  • add tests for task list reminder feature in basic and cqs
  • add tests for domain objects that verify that domain events are added correctly
  • write unit tests for core

About

A comparison of various approaches for implementing clean architecture in C#

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published