Chapter 4: Use Cases & Interactors
Note: This post originally contained 1 image that could not be recovered during the WordPress migration.
In my architecture the use-case command objects get served by so called Use-Case Interactors. Technically these are MediatR ÌRequestHandler which handle the use-case commands. Interactors orchestrate “the dance of the entities” as Uncle Bob says. This means that they do a mix of loading and saving entities from/to the persistence layer, call business logic using the entitiy-methods, calling other services, connect to external systems etc. They are orchestrators or moderators if you wish. Its one of the main parts for the application layer.
I you look at solution explorer you immediately see what use-cases this application addresses. This way even the business and domain-exports understand the scope of the application. This is one of the really cool things.
So let’s jump to a sample code of the use-case interactor which publishes a LogbookEntry for public viewing:
[UsedImplicitly]
internal class PublishLogbookEntryInteractor : IRequestHandler<PublishLogbookEntry, bool>
{
[NotNull] private readonly ILogger<PublishLogbookEntryInteractor> logger;
[NotNull] private readonly ILogbookEntryRepository dataAccess;
public PublishLogbookEntryInteractor(
[NotNull] ILogger<PublishLogbookEntryInteractor> logger,
[NotNull] ILogbookEntryRepository dataAccess)
{
this.dataAccess = dataAccess ?? throw new ArgumentNullException(nameof(dataAccess));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle([NotNull] PublishLogbookEntry request, CancellationToken cancellationToken)
{
if (request == null) throw new ArgumentNullException(nameof(request));
logger.LogInformation("Publish Logbook-Entry {logbookEntryId}", request.LogbookEntryId);
var logbookEntry = await dataAccess.FindByIdAsync(request.LogbookEntryId);
if (logbookEntry == null)
{
logger.LogError("Logbook-Entry {logbookEntryId} not found", request.LogbookEntryId);
return false;
}
try
{
logbookEntry.Publish();
await dataAccess.UpdateAsync(logbookEntry);
logger.LogInformation("Logbook-Entry {logbookEntryId} published successfull.", request.LogbookEntryId);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Error publishing logbook entry {logbookEntryId} {logbookEntryTitle}", logbookEntry.Id, logbookEntry.Title);
return false;
}
}
}
Notes:
-
The interactor implements
ÌRequestHandlerso MediatR use them to handlePublishLogbookEntryrequest that return aboolresult. -
Use
private readonlyfields that can be set only within the constructor of the class and therefore are immutable. -
The constructor parameters normally get injected by IoC container but in the unit-tests fakes are provided for them (see Dependency Inversion principal).
-
The constructor implementation uses C# 7 throw expressions to check if the value is not null. I also use R# annotations like
[NotNull[]and[UsedImplicitly]. These annotations help R# to help you by hinting where you might get a null-reference exception if you do not check fornull. -
Another thing I got from Clean Architecture is to make as much as possible accessible only
internal. Everything that I declarepublicis considered an external API that needs to be supported and therefore can not easily change. When everything in a assembly ispublicits a sign of a code-smell. Regarding Uncle Bob hiding things by not making them public is the biggest advantage for object-oriented programming (OOP).
Now that we have seen the application layer – or Application Business Rules as it is called CA – its time to go to the Enterprise Business Rules. Let’s look at the Entities and Domain Events here …