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.Responses

Cat.Responses models the outcome of an operation as a response: either succeeded, optionally carrying a value, or failed, carrying one or more errors.

Handlers across the other CAT modules (commands, queries, service requests) return ICatResponse or ICatResponse<TResponse>, so callers get one consistent return type instead of exceptions for outcomes the caller is expected to handle, like a missing resource or a failed validation.

Table of contents

NuGet packages

NuGet Version

Package Role Install Downloads
Cat.Responses.Abstractions Contracts dotnet add package gmarginet.Cat.Responses.Abstractions NuGet Downloads
Cat.Responses Implementation dotnet add package gmarginet.Cat.Responses NuGet Downloads

Namespaces

Namespace What it holds
Cat.Responses.Abstractions Response, paged list and error interfaces.
Cat.Responses The response types, the paged list and error definitions.

Dependencies

Module Why Cat.Responses needs it
Cat.Extensions CatPagedList checks its values with ThrowIfNull and ThrowIfLessThan.

Usage

Register the module

This module has nothing to register. Reference Cat.Responses.Abstractions in projects that only read responses. Reference Cat.Responses in projects that create them.

Response types

Every response has a Type of CatResponseType. Each type is either a Succeeded or a Failed variant, so you can tell the two apart without knowing the exact outcome. IsSucceeded and IsFailed do the same check for you.

Type Group Use it when Errors Value
Ok Succeeded The operation worked. None Optional
Created Succeeded The operation worked and made a new resource. None Optional
NoContent Succeeded The operation worked and has nothing to return. None None
BadRequest Failed The request was invalid, for example a failed validation. One or more None
NotFound Failed The requested resource doesn’t exist. One or more None
Unauthorized Failed The caller isn’t authenticated. One or more None
Forbidden Failed The caller is authenticated but isn’t allowed to do this. One or more None
Conflict Failed The request conflicts with the current state of the resource. One or more None
InternalServerError Failed Something unexpected went wrong. One or more None

Create a response

Use the static factory methods on CatResponse. Each type has two flavors:

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

// Succeeded, no value
ICatResponse ok = CatResponse.Ok();
ICatResponse created = CatResponse.Created();
ICatResponse noContent = CatResponse.NoContent();

// Succeeded, with a value
ICatResponse<Todo> okWithValue = CatResponse.Ok(todo);
ICatResponse<Todo> createdWithValue = CatResponse.Created(todo);
ICatResponse<Todo> noContentTyped = CatResponse.NoContent<Todo>();

// Failed, no value type. Pass one or more errors.
ICatResponse badRequest = CatResponse.BadRequest(error1, error2);
ICatResponse notFound = CatResponse.NotFound(error);
ICatResponse unauthorized = CatResponse.Unauthorized(error);
ICatResponse forbidden = CatResponse.Forbidden(error);
ICatResponse conflict = CatResponse.Conflict(error);
ICatResponse serverError = CatResponse.InternalServerError(error);

// Failed, typed to a value so it fits a handler that returns ICatResponse<Todo>
ICatResponse<Todo> typedNotFound = CatResponse.NotFound<Todo>(error);

Return a response from a handler, instead of throwing for an expected failure:

public async Task<ICatResponse<Todo>> HandleAsync(TodoGetQuery query, CancellationToken cancellationToken = default)
{
    var todo = await _demoDatabase.GetTodoAsync(query.TodoId!.Value, cancellationToken);

    return todo is null
        ? CatResponse.NotFound<Todo>(CatDemoErrorDefinitions.NotFound.CreateCatError("Todo", query.TodoId!.Value))
        : CatResponse.Ok(todo);
}

Create a list response

Use CatResponse.List for a response that carries a list of items. It returns ICatResponse<IReadOnlyList<TResponse>>, and it has the same outcomes as the other factories.

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

// Succeeded. Pass any IReadOnlyList<T>, such as an array or a List<T>.
ICatResponse<IReadOnlyList<Todo>> ok = CatResponse.List.Ok(todos);
ICatResponse<IReadOnlyList<Todo>> created = CatResponse.List.Created(todos);
ICatResponse<IReadOnlyList<Todo>> noContent = CatResponse.List.NoContent<Todo>();

// Failed. Pass one or more errors.
ICatResponse<IReadOnlyList<Todo>> badRequest = CatResponse.List.BadRequest<Todo>(error);
ICatResponse<IReadOnlyList<Todo>> notFound = CatResponse.List.NotFound<Todo>(error);
ICatResponse<IReadOnlyList<Todo>> unauthorized = CatResponse.List.Unauthorized<Todo>(error);
ICatResponse<IReadOnlyList<Todo>> forbidden = CatResponse.List.Forbidden<Todo>(error);
ICatResponse<IReadOnlyList<Todo>> conflict = CatResponse.List.Conflict<Todo>(error);
ICatResponse<IReadOnlyList<Todo>> serverError = CatResponse.List.InternalServerError<Todo>(error);

Create a paged list response

Use CatResponse.PagedList for one page of a larger list. It returns ICatResponse<ICatPagedList<TResponse>>. Build the page from values, or pass a page you already have.

// Build the page from values
ICatResponse<ICatPagedList<Todo>> ok = CatResponse.PagedList.Ok(
    todos, pageNumber: 2, pageSize: 10, totalItemCount: 25);

// Or pass an existing page
var page = new CatPagedList<Todo>(todos, 2, 10, 25);
ICatResponse<ICatPagedList<Todo>> created = CatResponse.PagedList.Created(page);

// No page, or a failure
ICatResponse<ICatPagedList<Todo>> noContent = CatResponse.PagedList.NoContent<Todo>();
ICatResponse<ICatPagedList<Todo>> notFound = CatResponse.PagedList.NotFound<Todo>(error);

BadRequest, Unauthorized, Forbidden, Conflict and InternalServerError work the same way as NotFound.

A page (ICatPagedList<T>) holds these values:

Property Meaning
List The items on this page.
PageNumber The number of this page. The first page is 1.
PageSize The maximum number of items on a page.
TotalItemCount The number of items across all pages.
TotalPages The number of pages needed to hold all items. It is calculated for you.

CatPagedList<T> checks its values when you set them. The list can’t be null, the page number and page size must be 1 or higher, and the total item count can’t be negative. Copy a page with a different value using with. The new value is checked too:

var nextPage = page with { PageNumber = page.PageNumber + 1 };

Two pages are equal when their numbers and their items are equal. The list is compared item by item, not by reference.

Read a response

Check the outcome with IsSucceeded and IsFailed. A failed response carries its errors in ErrorList. A failed ICatResponse<TResponse> has no value, so only read Value after you’ve checked IsSucceeded.

using Cat.Responses.Abstractions;

if (response.IsFailed)
{
    foreach (var error in response.ErrorList)
    {
        Console.WriteLine($"{error.Code}: {error.Message}");
    }
}

Match on Type when you need to react to the exact outcome:

var message = response.Type switch
{
    CatResponseType.Succeeded.Created => "Saved as a new item.",
    CatResponseType.Succeeded => "Done.",
    CatResponseType.Failed.NotFound => "We couldn't find it.",
    CatResponseType.Failed => "Something went wrong.",
    _ => "Unknown outcome."
};

Order the more specific patterns first, as in the example. The base types Succeeded and Failed catch everything the specific ones didn’t.

Cast a response

Cast moves a response from one value type to another. It follows two rules:

Cast(convertValue) on a failed ICatResponse<TResponse> is safe. It keeps the type and errors and doesn’t call the converter. The other casts throw an InvalidOperationException when they cross the line. See Errors and exceptions.

// Convert the value of a success. A failure passes through unchanged.
ICatResponse<TodoModel> modelResponse = todoResponse.Cast(todo => todo.ToModel());

Cast a failed response through to a different value type, for example when a decorator’s own check fails before it calls the inner handler:

return validationResponse.Type switch
{
    CatResponseType.Succeeded => await _innerHandler.HandleAsync(serviceRequest, cancellationToken),
    CatResponseType.Failed => validationResponse.Cast<TResponse>(),
    _ => CatResponse.InternalServerError<TResponse>(CatGeneralErrorDefinitions.UnexpectedResponse.CreateCatError())
};

Define and raise errors

An error has a Code, a Message template and Arguments. The template holds named placeholders. The arguments fill them in order.

Define reusable errors once, as static fields, instead of building CatError instances by hand everywhere:

using Cat.Responses;

public static class CatDemoErrorDefinitions
{
    public static readonly CatErrorDefinition NotFound = CatErrorDefinition.Create(
        "CAT-DEM-1000",
        "{ResourceName} with id {id} could not be found.");
}

// Raise it with the values for the placeholders
var error = CatDemoErrorDefinitions.NotFound.CreateCatError("Todo", 42);

For a one-off error, build it directly:

var error = CatError.Create("ORD-1001", "An order needs at least one line.");
var errorWithArguments = CatError.Create("ORD-1002", "Product '{productId}' is out of stock.", productId);

The module also ships three definitions for common cases:

Definition Code Message Arguments
CatGeneralErrorDefinitions.Unknown CAT-GEN-1000 An unknown error occurred. None
CatGeneralErrorDefinitions.UnexpectedResponse CAT-GEN-1001 An unexpected response was returned. ({Service}) Service
CatGeneralErrorDefinitions.ResourceNotFound CAT-GEN-1002 The requested {ResourceName} with id {ResourceId} was not found. ResourceName, ResourceId

Cat.Validators has its own definitions with the CAT-VAL- prefix, so its codes never clash with these. Give your application errors a prefix of your own, like CAT-DEM- in the demo.

Log the errors of a response

LogCatResponseErrors writes your message at error level, then one line per error as code: message:

using Cat.Responses.Abstractions.Extensions;

if (response.IsFailed)
{
    logger.LogCatResponseErrors(response, "Failed to process the request.");
}

Errors and exceptions

Responses exist so expected failures don’t become exceptions. The only exceptions this module throws come from Cast and from CatPagedList when you give it invalid values.

Situation What you get
Cast<TResponse>() on a succeeded response An InvalidOperationException with the message Cannot cast a succeeded response to a failed response.
Cast<TResponse>(value) on a failed ICatResponse An InvalidOperationException with the message Cannot cast a failed response to a succeeded response.
A paged list gets a null list An ArgumentNullException for List.
A paged list gets a page number or page size below 1 An ArgumentOutOfRangeException for PageNumber or PageSize, with the message <name> must be 1 or higher.
A paged list gets a negative total item count An ArgumentOutOfRangeException for TotalItemCount, with the message TotalItemCount must be 0 or higher.

The paged list exceptions also come from CatResponse.PagedList.Ok and Created when you build the page from values, and from a with copy.

Check IsFailed or IsSucceeded before you cast, as in the cast example above.

The failures a response reports are errors, not exceptions. Which codes you can get depends on the module that produced the response: