TODO: Add High level architecture and use cases image
Sample implementation of the Clean Architecture Principles with .NET Core. Use cases as central organizing structure, decoupled from frameworks and technology details. Has small components that are developed and tested in isolation.
Learn how to design modular applications.
Explore the .NET Core tooling.
Learning how to design modular applications will help you become a better engineer. Designing modular applications is the holy grail of software architecture, in our industry it is hard to find engineers that know how to design applications which allows adding new features in a steady speed.
.NET Core brings a sweet development environment, an extensible and cross-platform framework. We will explore the benefits of it in the infrastructure layer and we will reduce the importance of it in the domain. The same rule is applied for modern C# constructions.
This is continually updated, open source project.
Contributions are welcome!
Learn from the community.
Feel free to submit pull requests to help:
- Fix errors
- Improve sections
- Add new sections
- Submit questions and bugs
- Use Cases Description
- Flow of Control
- Architecture Styles
- Design Patterns
- Separation of Concerns
- Encapsulation
- Test-Driven Development TDD
- SOLID
- .NET Core
- DevOps
- Docker
- SQL Server
- Related Content and Projects
Application architecture is about usage, a good architecture screams the business use cases to the developer and framework concerns are implementation details. On Manga sample the user can Register
an account then manage the balance with Deposits
and Withdrawals
.
An customer can register the account using his personal details.
The customer can deposit a positive amount.
The customer can transfer money from one account to another.
A customer can withdraw money but not more that the current balance.
Customer details with all accounts and transactions are returned.
Account details with transactions are returned.
Close an account, requires zero balance.
The flow of control begins in the controller, moves through the use case, and then winds up executing in the presenter.
- An request in received by the
CustomersController
and an actionPost
is invoked. - The action creates an
RegisterInput
message and theRegister
use case is executed. - The
Register
use case creates aCustomer
and anAccount
. Repositories are called, theRegisterOutput
message is built and sent to theRegisterPresenter
. - The
RegisterPresenter
builds the HTTP Response message. - The
CustomersController
asks the presenter the current response.
- An request in received by the
CustomersController
and an actionGetCustomer
is invoked. - The action creates an
GetCustomerDetailsInput
message and theGetCustomerDetails
use case is executed. - The
GetCustomerDetails
use case asks the repositories about theCustomer
and theAccount
. It could call theNotFound
or theDefault
port of theGetCustomerDetailsPresenter
depending if it exists or not. - The
GetCustomerDetailsPresenter
builds the HTTP Response message. - The
CustomersController
asks the presenter the current response.
Manga use ideas from popular architectural styles. They Ports and Adapters are the simplest one followed by the others, they complement each other and aim a software with use cases decoupled from implementation details.
The Ports and Adapters Architecture Style divides the application into Application Core and Adapters in which the adapters are interchangeable components developed and tested in isolation. The Application Core is loosely coupled to the Adapters and their implementation details.
Very similar to Ports and Adapters, I would add that data objects cross boundaries as simple data structures. For instance, when the controller execute an use case it passes and immutable Input message. When the use cases calls an Presenter it gives a Output message (Data Transfer Objects if you like).
An application architecture implementation guided by tests cases.
The following Design Patterns will help you continue implementing use cases in a consistent way.
Controllers receive Requests, build an Input message then call an Use Case, you should notice that the controller do not build the ViewModel, instead this responsibility is delegated to the presenter object.
public sealed class CustomersController : Controller
{
// code omitted to simplify
public async Task<IActionResult> Post([FromBody][Required] RegisterRequest request)
{
await _registerUseCase.Execute(new RegisterInput(
new SSN(request.SSN),
new Name(request.Name),
new PositiveAmount(request.InitialAmount)));
return _presenter.ViewModel;
}
}
ViewModels are data transfer objects, they will be rendered by the MVC framework so we need to follow the framework guidelines. I suggest that you add [Required]
attributes so swagger generators could know the properties that are not nullable. My personal preference is to avoid getters here because you know how the response object will be created, so use the constructor.
/// <summary>
/// The response for Registration
/// </summary>
public sealed class RegisterResponse
{
/// <summary>
/// Customer ID
/// </summary>
[Required]
public Guid CustomerId { get; }
/// <summary>
/// SSN
/// </summary>
[Required]
public string SSN { get; }
/// <summary>
/// Name
/// </summary>
[Required]
public string Name { get; }
/// <summary>
/// Accounts
/// </summary>
[Required]
public List<AccountDetailsModel> Accounts { get; }
public RegisterResponse(
Guid customerId,
string ssn,
string name,
List<AccountDetailsModel> accounts)
{
CustomerId = customerId;
SSN = ssn;
Name = name;
Accounts = accounts;
}
}
Presenters build the Response objects when called by the application.
public sealed class RegisterPresenter : IOutputPort
{
public IActionResult ViewModel { get; private set; }
public void Error(string message)
{
var problemDetails = new ProblemDetails()
{
Title = "An error occurred",
Detail = message
};
ViewModel = new BadRequestObjectResult(problemDetails);
}
public void Standard(RegisterOutput output)
{
/// long object creation omitted
ViewModel = new CreatedAtRouteResult("GetCustomer",
new
{
customerId = model.CustomerId
},
model);
}
}
It is important to understand that from the Application perspective the use cases see an OutputPort with custom methods to call dependent on the message, and from the Web Api perspective the Controller only see the ViewModel property.
The output port for the use case regular behavior.
Called when an blocking errors happens.
Called when an blocking errors happens.
Describe the tiny domain business rules. Objects that are unique by the has of their properties. Are immutable.
public sealed class Name : IEquatable<Name>
{
private string _text;
private Name() { }
public Name(string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new NameShouldNotBeEmptyException("The 'Name' field is required");
_text = text;
}
public override string ToString()
{
return _text;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj is string)
{
return obj.ToString() == _text;
}
return ((Name) obj)._text == _text;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + _text.GetHashCode();
return hash;
}
}
public bool Equals(Name other)
{
return this._text == other._text;
}
}
Objects that are unique by their IDs. Entities are mutable and instances of domain concepts.
Run the EF Tool to add a migration to the Manga.Infrastructure
project.
$ dotnet ef migrations add "InitialCreate" -o "EntityFrameworkDataAccess/Migrations" --project source/Manga.Infrastructure --startup-project source/Manga.WebApi
Generate tables and seed the database via Entity Framework Tool:
dotnet ef database update --project source/Manga.Infrastructure --startup-project source/Manga.WebApi
Manga is a cross-platform application, you can run it from Mac, Windows or Unix. To develop new features, you may use Visual Studio or Visual Studio Code ❤️.
The single requirement is to install the latest .NET Code SDK.
We made avaiable scripts to create and seed the database quickly via Docker.
Finally to run it locally use:
$ dotnet run --project "source/Manga.WebApi/Manga.WebApi.csproj"
To spin up a SQL Server in a docker container using the connection string Server=localhost;User Id=sa;Password=<YourNewStrong!Passw0rd>;
run the following command:
$ ./source/scripts/sql-docker-up.sh
Thanks goes to these wonderful people (emoji key):
Ivan Paulovich 🎨 |
Petr Sedláček |
Gus 🎨 |
arulconsultant |
Guilherme Silva 🎨 |
Ondřej Štorc 🎨 |
Marlon Miranda da Silva 🎨 |
NicoCG |
Filipe Augusto Lima de Souza 🎨 |
sshaw-sml |
This project follows the all-contributors specification. Contributions of any kind welcome!