View on GitHub
Clean Architecture Toolkit for .NET
Clean Architecture Toolkit is a clean, modular architecture foundation for .NET applications.
It embraces SOLID principles and focuses on maintainability, testability, and scalability through clear boundaries and pluggable design.

|   HOME   |   MODULE OVERVIEW   |   DEMO   |

Cat.ServiceRequests

Cat.ServiceRequests gives you one entry point for running application use cases. You describe a use case as a service request, write one handler for it, and hand the request to the processor. The processor finds the handler for you and returns an ICatResponse.

Handlers can be wrapped in decorators for cross-cutting work. The module ships three: validation, logging and activity tracing. You switch each one on only when you need it.

Table of contents

NuGet packages

NuGet Version

Package Role Install Downloads
Cat.ServiceRequests.Abstractions Contracts dotnet add package gmarginet.Cat.ServiceRequests.Abstractions NuGet Downloads
Cat.ServiceRequests Implementation dotnet add package gmarginet.Cat.ServiceRequests NuGet Downloads
Cat.ServiceRequests.Decorators.ActivitySource Optional decorator dotnet add package gmarginet.Cat.ServiceRequests.Decorators.ActivitySource NuGet Downloads
Cat.ServiceRequests.Decorators.Logging Optional decorator dotnet add package gmarginet.Cat.ServiceRequests.Decorators.Logging NuGet Downloads
Cat.ServiceRequests.Decorators.Validation Optional decorator dotnet add package gmarginet.Cat.ServiceRequests.Decorators.Validation NuGet Downloads
Cat.ServiceRequests.Minimal.Api Optional integration dotnet add package gmarginet.Cat.ServiceRequests.Minimal.Api NuGet Downloads

Namespaces

Namespace What it holds
Cat.ServiceRequests.Abstractions Request, handler and processor interfaces.
Cat.ServiceRequests The processor and the module registration.
Cat.ServiceRequests.Decorators.ActivitySource The activity tracing decorator for handlers.
Cat.ServiceRequests.Decorators.Logging The logging decorator for handlers.
Cat.ServiceRequests.Decorators.Validation The validation decorator for handlers.
Cat.ServiceRequests.Minimal.Api The service request for a JSON Patch document.

Dependencies

Module Why Cat.ServiceRequests needs it
Cat.Responses Handlers return an ICatResponse.
Cat.Decorators Builds the handlers and runs their decorators.
Cat.Validators The validation decorator checks requests with validators.

Usage

Register the module

Call AddCatServiceRequestsModule and pass one type from each assembly that holds handlers. The module scans those assemblies and registers every non-generic handler with a scoped lifetime. It also registers ICatServiceRequestProcessor.

The module needs the Cat.Decorators module to build its handlers. If you haven’t registered Cat.Decorators yet, AddCatServiceRequestsModule does it for you.

using Cat.ServiceRequests.Extensions;

builder.Services.AddCatServiceRequestsModule(typeof(TodoGetRequestHandler));

Handlers live in more than one assembly? Pass one type per assembly:

builder.Services.AddCatServiceRequestsModule(typeof(TodoGetRequestHandler), typeof(OrderPlaceRequestHandler));

Define a request

A request is a plain type that implements one of two interfaces:

using Cat.ServiceRequests.Abstractions;

// With a payload
public record TodoGetRequest(int? TodoId) : ICatServiceRequest<TodoModel>;

// Without a payload
public record TodoSaveRequest(TodoModel Todo) : ICatServiceRequest;

Write a handler

Implement the matching handler interface once per request type. Return a response instead of throwing for failures the caller should expect.

With a payload, implement ICatServiceRequestHandler<TRequest, TResponse>:

using Cat.Responses;
using Cat.Responses.Abstractions;
using Cat.ServiceRequests.Abstractions;

public class TodoGetRequestHandler : ICatServiceRequestHandler<TodoGetRequest, TodoModel>
{
    private readonly ICatQueryProcessor _queryProcessor;

    public TodoGetRequestHandler(ICatQueryProcessor queryProcessor)
    {
        _queryProcessor = queryProcessor;
    }

    public async Task<ICatResponse<TodoModel>> HandleAsync(TodoGetRequest serviceRequest, CancellationToken cancellationToken = default)
    {
        var response = await _queryProcessor.ProcessAsync(serviceRequest.ToQuery(), cancellationToken);

        return response.Cast(x => x.ToModel());
    }
}

Without a payload, implement ICatServiceRequestHandler<TRequest>:

public class TodoSaveRequestHandler : ICatServiceRequestHandler<TodoSaveRequest>
{
    private readonly ICatCommandProcessor _commandProcessor;

    public TodoSaveRequestHandler(ICatCommandProcessor commandProcessor)
    {
        _commandProcessor = commandProcessor;
    }

    public async Task<ICatResponse> HandleAsync(TodoSaveRequest serviceRequest, CancellationToken cancellationToken = default)
    {
        return await _commandProcessor.ProcessAsync(serviceRequest.ToCommand(), cancellationToken);
    }
}

A handler passes the failed response of a query or command straight on. Use Cast to give it the payload type of the handler.

Process a request

Inject ICatServiceRequestProcessor and pass it the request. Each call opens its own scope, so the handler and its dependencies live only as long as the request.

using Cat.ServiceRequests.Abstractions;

public class TodoEndpoints
{
    private readonly ICatServiceRequestProcessor _processor;

    public TodoEndpoints(ICatServiceRequestProcessor processor)
    {
        _processor = processor;
    }

    public async Task<ICatResponse<TodoModel>> GetAsync(int id, CancellationToken cancellationToken)
    {
        return await _processor.ProcessAsync(new TodoGetRequest(id), cancellationToken);
    }

    public async Task<ICatResponse> SaveAsync(TodoModel todo, CancellationToken cancellationToken)
    {
        return await _processor.ProcessAsync(new TodoSaveRequest(todo), cancellationToken);
    }
}

Prefer the async methods. The Process methods block until the handler finishes, so keep them for places where you can’t await.

var response = _processor.Process(new TodoGetRequest(id));

To expose a request as an HTTP endpoint, see Cat.Minimal.Api.

Handle a JSON Patch document

The Cat.ServiceRequests.Minimal.Api package holds PatchDocumentRequest<TRouteParameters, TModel>. It is a request that carries a JSON Patch document and the route values that identify the resource. Cat.Minimal.Api maps it with MapCatPatchDocument.

The package targets net10.0, because the patch types come from Microsoft.AspNetCore.JsonPatch.SystemTextJson.

Write a handler for the request like any other. Apply the patch to the model you loaded, then save it:

using Cat.ServiceRequests.Minimal.Api;

public record TodoRouteParameters(int TodoId);

public class TodoPatchRequestHandler : ICatServiceRequestHandler<PatchDocumentRequest<TodoRouteParameters, TodoModel>>
{
    private readonly ITodoStore _todoStore;

    public TodoPatchRequestHandler(ITodoStore todoStore)
    {
        _todoStore = todoStore;
    }

    public async Task<ICatResponse> HandleAsync(PatchDocumentRequest<TodoRouteParameters, TodoModel> serviceRequest, CancellationToken cancellationToken = default)
    {
        var todo = await _todoStore.GetAsync(serviceRequest.RouteParameters.TodoId, cancellationToken);

        if (todo is null)
        {
            return CatResponse.NotFound(CatDemoErrorDefinitions.NotFound.CreateCatError("Todo", serviceRequest.RouteParameters.TodoId));
        }

        serviceRequest.PatchDocument.ApplyTo(todo);
        await _todoStore.SaveAsync(todo, cancellationToken);

        return CatResponse.NoContent();
    }
}

Add decorators

Decorators run through the Cat.Decorators module. AddCatServiceRequestsModule already registers it, so you don’t have to.

Each decorator has its own registration method. Call it on the built service provider, after Build(). Decorators wrap the handler in the order you register them. The last one you register is the outermost, so it runs first.

using Cat.ServiceRequests.Decorators.ActivitySource.Extensions;
using Cat.ServiceRequests.Decorators.Logging.Extensions;
using Cat.ServiceRequests.Decorators.Validation.Extensions;

var app = builder.Build();

app.Services.AddCatServiceRequestsModuleValidationDecorator();
app.Services.AddCatServiceRequestsModuleLoggingDecorator();
app.Services.AddCatServiceRequestsModuleActivitySourceDecorator();

With this order a request runs the activity, then logging, then validation, then your handler. Register only the ones you need.

Validation

Runs the validators of the request before the handler. An invalid request never reaches the handler. The caller gets the validation failure instead.

Pass validationRequired: true to treat a request without a validator as a failure too. The default is false, so a request without a validator goes straight to the handler.

// Requests without a validator are allowed
app.Services.AddCatServiceRequestsModuleValidationDecorator();

// Requests without a validator fail
app.Services.AddCatServiceRequestsModuleValidationDecorator(validationRequired: true);

Write the validator with Cat.Validators.

Logging

Logs the start and end of every handler at debug level. A failed response logs its errors.

ActivitySource

Starts an activity on the Cat.ServiceRequests source around every handler, tagged with the request type. Add that source to your OpenTelemetry setup to collect the traces:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource("Cat.ServiceRequests"));

Errors and exceptions

The processor never turns a failure into an exception for you. What you get back depends on where the problem sits.

Situation What you get
The handler returns Ok, Created or NoContent A succeeded response.
The handler returns BadRequest, NotFound, Unauthorized, Forbidden, Conflict or InternalServerError A failed response with the errors the handler passed. It has no payload.
The validation decorator finds an invalid request A BadRequest response with one error for each failed rule. The handler never runs. See the rule codes in Cat.Validators.
The validation decorator is on with validationRequired: true and the request has no validator An InvalidOperationException from the service provider, because the validator for the request isn’t registered.
The validator processor returns a response that is neither succeeded nor failed An InternalServerError with CAT-GEN-1001 and the message An unexpected response was returned. ({Service}). This should not happen with the built-in processor.
No handler is registered for the request An InvalidOperationException with the message No decorated service found for service type ICatServiceRequestHandler<YourRequest>. Check that AddCatServiceRequestsModule got a type from the handler’s assembly.
The handler throws The original exception reaches the caller as it is. It isn’t wrapped in a TargetInvocationException.

Over HTTP, Cat.Minimal.Api turns each of these into a status code and a problem details body.