Offside

Messages and cultures

Português · Back to docs

Error text lives in JSON catalogs, one per culture. C# holds the metadata — code, kind, arguments — and never the wording.

Catalog format

A flat JSON object mapping error code to message template:

{
  "not_found": "{resource} '{id}' was not found.",
  "conflict": "Conflict on {resource}.",
  "validation": "{field} is invalid.",
  "unexpected": "An unexpected error occurred."
}

The eleven default codes — not_found, gone, conflict, validation, bad_request, unauthorized, forbidden, precondition_failed, unprocessable, too_many_requests, unexpected — cover every built-in factory. Add one entry per custom code you introduce with Error.Custom.

Registration

builder.Services.AddOffside(options =>
{
    options.AddJson(CultureInfo.InvariantCulture, File.ReadAllText("errors/errors.json"));
    options.AddJson(new CultureInfo("pt-BR"),     File.ReadAllText("errors/errors.pt-BR.json"));
    options.AddJson(new CultureInfo("es"),        File.ReadAllText("errors/errors.es.json"));
});

AddJson takes the catalog content, not a path. There is also a Stream overload for embedded resources:

options.AddJson(CultureInfo.InvariantCulture,
    typeof(Program).Assembly.GetManifestResourceStream("MyApp.errors.json")!);

Catalogs are parsed once, at registration. A malformed file fails at startup, not on the first failing request.

An invariant-culture catalog is required. Without it, AddOffside throws InvalidOperationException. It is the final fallback, so every code should appear there.

Culture fallback

Resolution tries three lookups in order and stops at the first hit:

  1. The exact culture — pt-BR
  2. Its parent — pt
  3. The invariant catalog

So a pt catalog serves pt-BR and pt-PT alike, and you translate the specific ones only where the wording actually differs. If no catalog defines the code at all, the resolver returns the code itself — a response stays well-formed and the missing entry is visible rather than silently blank.

In an ASP.NET Core host the culture comes from the Accept-Language header unless you pass one explicitly. See Cultures.

Interpolation

Template tokens are {name}, filled from Error.Arguments:

Error.NotFound("order", 42)
// arguments: resource = "order", id = 42
{ "not_found": "{resource} '{id}' was not found." }
order '42' was not found.

Three behaviours to know:

Adding a language

  1. Copy errors/errors.json to errors/errors.<culture>.json.
  2. Translate the values. Leave the keys and the {tokens} untouched.
  3. Register it with options.AddJson(new CultureInfo("<culture>"), ...).
{
  "not_found": "{resource} '{id}' não foi encontrado.",
  "conflict": "Conflito em {resource}.",
  "validation": "{field} é inválido.",
  "unexpected": "Ocorreu um erro inesperado."
}

A translated catalog does not have to be complete. Anything it omits falls back to the parent culture and then to the invariant catalog, so you can ship a partial translation and fill it in over time.

offside init writes an English and a Brazilian Portuguese catalog to start from — see the CLI page.

Custom resolvers

AddOffside registers a JsonErrorMessageResolver as the singleton IErrorMessageResolver. To source messages from somewhere else — a database, satellite resource assemblies, a translation service — implement the interface and register it instead:

public sealed class ResxErrorMessageResolver : IErrorMessageResolver
{
    public string GetMessage(Error error, CultureInfo culture) =>
        Messages.ResourceManager.GetString(error.Code, culture) ?? error.Code;
}

builder.Services.AddSingleton<IErrorMessageResolver, ResxErrorMessageResolver>();

Do not call AddOffside in that case — it would register the JSON resolver alongside yours. Returning error.Code for an unknown code is the convention worth keeping: it degrades to something diagnosable instead of an empty string.