Chapter 4: Use-Cases & Interactors

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
ÌRequestHandler
so MediatR use them to handlePublishLogbookEntry
request that return abool
result. -
Use
private readonly
fields 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 declarepublic
is considered an external API that needs to be supported and therefore can not easily change. When everything in a assembly ispublic
its 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 …
Categories