Architecture
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 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 and exposes AddMiddlewares(Type[]), which registers each given
middleware type on the app in the order supplied, skipping any type that isn’t a WebApiMiddleware.
A typical setup registers the three built-in middlewares in this order:
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 AuthenticatedUser</i>"]
Auth --> Endpoint["Controller / endpoint"]
Endpoint --> Resolver["ResponseResolver<br/><i>Output envelope → ActionResult</i>"]
Resolver --> Client
WebApiMiddleware is an abstract marker base class with no members — its only job is to let
AddMiddlewares recognize which types are safe to register with App.UseMiddleware(...):
AddMiddlewares([
typeof(TraceActivityMiddleware),
typeof(ExceptionMiddleware),
typeof(AuthenticationMiddleware)
]);
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["AuthenticatedUserFactory.FromToken<br/><i>id + role claims, no lookup</i>"]
Mode -- "Revalidate" --> ProviderId["IAuthenticationProvider.GetAuthenticatedUserById<br/><i>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>403 if role not authorized</i>"]
RoleReq --> Endpoint["Controller action"]
For the app JWT scheme, ClaimsOnly (the default) never touches a data store — the user is rebuilt
straight from the token’s id and role claims, so authentication costs nothing beyond the signature
check. Revalidate instead resolves IAuthenticationProvider from the current request’s
HttpContext.RequestServices and calls GetAuthenticatedUserById 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] 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
ResponseResolver.Resolve(...) 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 ResponseResolver.Resolve(output);
}
See Responses for the full mapping reference.
Design principles⌗
- Envelopes, not exceptions. Endpoints report failure through
ProcessOutput/DataOutput<T>and letResponseResolverpick the status code;ExceptionMiddlewareis 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.ClaimsOnlyis 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,ExceptionMiddlewareandAuthenticationMiddlewaredoes exactly one thing and derives from theWebApiMiddlewaremarker so it can be composed viaAddMiddlewaresin whatever order a given host needs. - Consistent
InvokeAsync. Every middleware follows the standard ASP.NET Core convention — a constructor capturingRequestDelegate nextplus other dependencies, and a singleInvokeAsync(HttpContext)method — so custom middlewares slot intoAddMiddlewaresthe same way the built-in ones do. - DI-first. Dependencies such as
IAuthenticationProvider,AuthenticationOptions, theITokenValidators andSettingsProviderare resolved through the container (constructor injection or, for per-request freshness,HttpContext.RequestServices) rather than passed around manually.
Where to next⌗
- Configuration — the
WebApiStartuplifecycle andWebApiParameters. - 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
ResponseResolvermapping reference. - Endpoint Toggling — enabling or disabling individual endpoints from code or configuration.