-
-
Notifications
You must be signed in to change notification settings - Fork 694
Design Patterns
The following Design Patterns will help you continue implementing use cases in a consistent way.
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;
}
}
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;
}
}
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.
The output port for the use case regular behavior.
Called when an blocking errors happens.
Called when an blocking errors happens.
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);
}
}
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;
}
}
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;
}
}
- Value Object
- Entity
- Aggregate Root
- Repository
- Use Case
- Bounded Context
- Entity Factory
- Domain Service
- Application Service
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Swagger and API Versioning
- Microsoft Extensions
- Feature Flags
- Logging
- Data Annotations
- Authentication
- Authorization