Application orchestration and CQRSThe thin use case
No narration yet
Module 6 · Lesson 112 min

The thin use case

An application use case is deliberately thin. It's a sealed class with its ports injected through the constructor and one method, Execute, that returns a Result. Its job is orchestration: guard the request at the trust boundary, build the domain objects, call the port, return the Result. The domain rules live in the domain; the IO lives behind ports; the use case just conducts.

The fan-out use case (application/flight-notes/SaveFlightNote.cs)
public sealed class SaveFlightNote
{
    private static readonly EjError NoFlightKeys =
        new(nameof(SaveFlightNoteRequest.EjdpUniqueFlightKeys), "At least one flight is required.");

    private readonly IFlightNoteRepository _flightNoteRepository;

    public SaveFlightNote(IFlightNoteRepository flightNoteRepository) =>
        _flightNoteRepository = flightNoteRepository;

    [Trace]
    public async Task<Result<SaveFlightNoteResult>> Execute(
        SaveFlightNoteRequest request, CancellationToken cancellationToken)
    {
        if (request.EjdpUniqueFlightKeys is not { Count: > 0 })
            return Result<SaveFlightNoteResult>.Failure(NoFlightKeys);

        var notes = new List<FlightNoteVo>(request.EjdpUniqueFlightKeys.Count);
        var errors = new List<EjError>();
        foreach (var key in request.EjdpUniqueFlightKeys)
        {
            var noteResult = FlightNoteVo.Create(key, request.Body);
            if (noteResult.IsSuccess) notes.Add(noteResult.Value);
            else errors.AddRange(noteResult.Errors);
        }

        if (errors.Count > 0)
            return Result<SaveFlightNoteResult>.Failure(errors.ToArray());

        await _flightNoteRepository.SaveMany(notes, cancellationToken);
        return Result<SaveFlightNoteResult>.Success(
            new SaveFlightNoteResult(notes.Select(n => n.EjdpUniqueFlightKey).ToArray()));
    }
}

Two real details worth copying. The trust-boundary guard lives IN the use case, not in decorative DTO attributes: the code comment notes the DTO's [Required] annotations are inert because no validation filter is wired, so the real check is the is not { Count: > 0 } guard here. And this use case accumulates its own errors in a loop rather than a FailIf chain, because it needs to build a List as it goes, which is exactly the "imperative is fine when the fluent chain is structurally incompatible" exception (convention D5, anti-pattern #21).

Practice

Try it yourself

Check

Check the use-case shape

Verify you can recognise the anatomy.

You should see

Reading SaveFlightNote, you can point to each part: sealed class; a ctor-injected IFlightNoteRepository port (an interface, not a concrete type); an Execute(request, CancellationToken) returning Task<Result<...>>; a trust-boundary guard first; domain objects built via FlightNoteVo.Create; a single port call (SaveMany); and a Result returned on every path. No SQL, no domain calculation, no new FlightNote() DBO in sight.

Recall

Where the trust-boundary guard lives

A subtle real-world point.

In SaveFlightNote, why is the "at least one flight key" check written as an explicit guard in Execute rather than relying on a [Required]/[MinLength] attribute on the request DTO?