The following Design Patterns will help you continue implementing use cases in a consistent way.
Controller
Controllers receive Requests, build the Input message then call the Use Case, you should notice that the controller do not build the Response, 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;
}
}
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 comments describing each property and the [Required]
attribute so swagger generators could know the properties that are not nullable. My personal preference is to avoid setters here because you have total control of response object instantiation, so implement 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;
}
}
Presenter
Presenters are called by the application Use Cases and build the Response objects.
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.
Standard Output
The output port for the use case regular behavior.
Error Output
Called when an blocking errors happens.
Alternative Output
Called when an blocking errors happens.
Unit of Work
public interface IUnitOfWork
{
Task<int> Save();
}
public sealed class UnitOfWork : IUnitOfWork, IDisposable
{
private MangaContext context;
public UnitOfWork(MangaContext context)
{
this.context = context;
}
public async Task<int> Save()
{
int affectedRows = await context.SaveChangesAsync();
return affectedRows;
}
private bool disposed = false;
private void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
First-Class Collections
public sealed class CreditsCollection
{
private readonly IList<ICredit> _credits;
public CreditsCollection()
{
_credits = new List<ICredit>();
}
public void Add<T>(IEnumerable<T> credits)
where T : ICredit
{
foreach (var credit in credits)
Add(credit);
}
public void Add(ICredit credit)
{
_credits.Add(credit);
}
public IReadOnlyCollection<ICredit> GetTransactions()
{
var transactions = new ReadOnlyCollection<ICredit>(_credits);
return transactions;
}
public PositiveAmount GetTotal()
{
PositiveAmount total = new PositiveAmount(0);
foreach (ICredit credit in _credits)
{
total = credit.Sum(total);
}
return total;
}
}
Factory
public interface IEntityFactory
{
ICustomer NewCustomer(SSN ssn, Name name);
IAccount NewAccount(ICustomer customer);
ICredit NewCredit(IAccount account, PositiveAmount amountToDeposit);
IDebit NewDebit(IAccount account, PositiveAmount amountToWithdraw);
}
public sealed class EntityFactory : IEntityFactory
{
public IAccount NewAccount(ICustomer customer)
{
var account = new Account(customer);
return account;
}
public ICredit NewCredit(IAccount account, PositiveAmount amountToDeposit)
{
var credit = new Credit(account, amountToDeposit);
return credit;
}
public ICustomer NewCustomer(SSN ssn, Name name)
{
var customer = new Customer(ssn, name);
return customer;
}
public IDebit NewDebit(IAccount account, PositiveAmount amountToWithdraw)
{
var debit = new Debit(account, amountToWithdraw);
return debit;
}
}
Component
Index of Clean Architecture Manga
Home
Use Cases
Flow of Control
Architecture Styles
Design Patterns
Domain-Driven Design Patterns
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
Separation of Concerns
Encapsulation
Test-Driven Development TDD
Fakes
SOLID
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
.NET Core Web API
- Swagger and API Versioning
- Microsoft Extensions
- Feature Flags
- Logging
- Data Annotations
- Authentication
- Authorization
Entity Framework Core
Environment Configurations
DevOps
- Running the Application Locally
- Running the Tests Locally
- Continuous Integration & Continuous Deployment