Architecture

This page shows how the pieces of ArturRios.Util.WebApi fit together: the request pipeline, the security model, the response envelope, and the design…

This page shows how the pieces of ArturRios.Util.WebApi fit together: the request pipeline, the security model, the response envelope, and the design principles that hold them together.

Request pipeline

The library is a thin layer around the standard ASP.NET Core pipeline. WebApiStartup builds the WebApplicationBuilder/WebApplication through one standard sequence, whose pipeline half adds the three built-in middlewares with UseStandardMiddlewares() — AuthenticationMiddleware only when AddTokenAuthentication was registered, with Swagger and CORS (when enabled) served just before it — then any extra middlewares from WebApiStartupOptions.Middlewares, then the controllers:

flowchart LR
    Client["Client request"] --> Trace["TraceActivityMiddleware<br/><i>assigns/propagates W3C traceparent</i>"]
    Trace --> Exception["ExceptionMiddleware<br/><i>catches unhandled exceptions</i>"]
    Exception --> Auth["AuthenticationMiddleware<br/><i>validates token, attaches IAuthenticatedUser</i>"]
    Auth --> Endpoint["Controller / endpoint"]
    Endpoint --> Resolver["ToActionResult<br/><i>Output envelope → ActionResult</i>"]
    Resolver --> Client

WebApiMiddleware is an abstract marker base class with no members — its only job is to let UseWebApiMiddlewares (which WebApiStartup runs over Options.Middlewares) recognize which types are safe to register with app.UseMiddleware(...). It registers each type in the order supplied, and throws an ArgumentException for any type that isn’t a WebApiMiddleware:

public class Startup(string[] args) : WebApiStartup(args, options =>
{
    options.Middlewares.Add(typeof(MyMiddleware)); // runs after the three built-in ones
})
{
    // ...
}

// or, on a plain WebApplication:
app.UseStandardMiddlewares();
app.UseWebApiMiddlewares(typeof(MyMiddleware));

Because registration order is registration order, TraceActivityMiddleware runs first so the trace id is available to everything downstream (including exception logging), ExceptionMiddleware runs next so it can catch exceptions thrown by authentication or the endpoint itself, and AuthenticationMiddleware runs last of the three so only requests that pass tracing/exception setup pay for token validation.

See Middleware & Diagnostics for what TraceActivityMiddleware and ExceptionMiddleware do in detail, and Configuration for the full WebApiStartup lifecycle.

Security model

AuthenticationMiddleware extracts a token (from the header, a cookie, or either, per AuthenticationOptions.Source) and validates it on every request that isn’t a Swagger route or an [AllowAnonymous] endpoint. Which token schemes are tried, and how, depends on AuthenticationOptions (registered via AddTokenAuthentication):

flowchart TB
    Token["Extracted token"] --> Jwt{"EnableJwt?"}
    Jwt -- "yes" --> JwtValid{"Signature valid?"}
    JwtValid -- "no" --> Google
    JwtValid -- "yes" --> Mode{"JwtMode"}
    Mode -- "ClaimsOnly (default)" --> Claims["mapper.FromClaims<br/><i>your claims, no lookup</i>"]
    Mode -- "Revalidate" --> ProviderId["IAuthenticationProvider.GetAuthenticatedUserById<br/><i>Guid id, resolved per-request from RequestServices</i>"]

    Jwt -- "no" --> Google{"EnableGoogle?"}
    Google -- "yes" --> GoogleValid{"Google ID token valid?"}
    GoogleValid -- "yes" --> ProviderEmail["IAuthenticationProvider.GetAuthenticatedUserByEmail"]
    GoogleValid -- "no" --> R401["401 Unauthorized"]
    Google -- "no" --> R401

    ProviderId -.-> Cached["CachedAuthenticationProvider<br/><i>IMemoryCache, Ttl / CacheMisses</i>"]
    ProviderEmail -.-> Cached
    Cached -.-> ProviderId
    Cached -.-> ProviderEmail

    Claims --> Items["HttpContext.Items[User]"]
    ProviderId --> Items
    ProviderEmail --> Items

    Items --> Authorize["AuthorizeAttribute<br/><i>401 if no user</i>"]
    Authorize --> RoleReq["RoleRequirementFilter<br/><i>401 if no user, 403 if role not authorized</i>"]
    RoleReq --> Endpoint["Controller action"]

For the app JWT scheme, ClaimsOnly (the default) never touches a data store — the registered mapper rebuilds the user straight from the token’s claims, so authentication costs nothing beyond the signature check. Revalidate instead resolves IAuthenticationProvider from the current request’s HttpContext.RequestServices and calls GetAuthenticatedUserById with the Guid from the token’s claims on every request, trading a lookup for freshness. The Google scheme always resolves the user via IAuthenticationProvider.GetAuthenticatedUserByEmail after verifying the token, so an IAuthenticationProvider is required whenever Google authentication is enabled, just as it is for JWT Revalidate. CachedAuthenticationProvider sits transparently in front of any IAuthenticationProvider (registered via AddCachedAuthenticationProvider<T>) to absorb repeated lookups of the same user within a short TTL, without any mode needing to know it’s there.

AuthorizeAttribute and RoleRequirementFilter both read the same HttpContext.Items["User"] slot that AuthenticationMiddleware populates, and both honor [AllowAnonymous] — the library’s or ASP.NET Core’s own, on the action or the controller — by short-circuiting before checking it.

See Security for the full authentication and authorization reference.

Response envelopes

Endpoints return an ArturRios.Output envelope rather than throwing on failure. ProcessOutput carries success/error/message state; DataOutput<T> adds a typed payload on top of it:

classDiagram
    class ProcessOutput {
        +bool Success
        +List~string~ Messages
        +List~string~ Errors
        +WithError(string) ProcessOutput
    }
    class DataOutput~T~ {
        +T Data
        +WithData(T) DataOutput~T~
    }
    class PaginatedOutput~T~ {
        +int Page
        +int PageSize
        +int TotalCount
    }
    ProcessOutput <|-- DataOutput
    DataOutput <|-- PaginatedOutput

The ToActionResult(...) extension maps any of ProcessOutput, DataOutput<T> or PaginatedOutput<T> onto an ActionResult, defaulting the HTTP status to 200 when Success is true and 400 otherwise, unless an explicit statusCode is supplied:

[HttpGet("{id:int}")]
public ActionResult<DataOutput<UserDto?>> GetById(int id)
{
    DataOutput<UserDto?> output = _userService.GetById(id);

    return output.ToActionResult();
}

See Responses for the full mapping reference.

Design principles

  • Envelopes, not exceptions. Endpoints report failure through ProcessOutput/DataOutput<T> and let ToActionResult pick the status code; ExceptionMiddleware is the safety net for anything that still escapes as a thrown exception, not the primary error-reporting path.
  • Stateless by default, revalidating when you need it. JwtValidationMode.ClaimsOnly is the default because most requests don’t need a fresh database read on every call; Revalidate (optionally cached) is there for the cases where staleness — instead of an expired token — is the risk you can’t accept.
  • Small, focused middlewares. Each of TraceActivityMiddleware, ExceptionMiddleware and AuthenticationMiddleware does exactly one thing and derives from the WebApiMiddleware marker so it can be composed via UseWebApiMiddlewares in whatever order a given host needs.
  • Consistent InvokeAsync. Every middleware follows the standard ASP.NET Core convention — a constructor capturing RequestDelegate next plus other dependencies, and a single InvokeAsync(HttpContext) method — so custom middlewares slot into WebApiStartupOptions.Middlewares (or UseWebApiMiddlewares) the same way the built-in ones do.
  • Opinionated, but not locked in. WebApiStartup runs one standard sequence and asks only for ConfigureServices; every step of that sequence is also a public extension method, so a host that needs a different order composes the same pieces on a plain WebApplicationBuilder.
  • DI-first. Dependencies such as IAuthenticationProvider, AuthenticationOptions, the ITokenValidators and SettingsProvider are resolved through the container (constructor injection or, for per-request freshness, HttpContext.RequestServices) rather than passed around manually.

Where to next

  • Configuration — the WebApiStartup lifecycle and WebApiParameters.
  • Security — JWT validation modes, caching, and role-based authorization in depth.
  • Middleware & Diagnostics — exception handling and distributed tracing.
  • HTTP Client — building typed clients on top of BaseWebApiClient.
  • Responses — the full ToActionResult mapping reference.
  • Endpoint Toggling — enabling or disabling individual endpoints from code or configuration.