Skip to content

SAIF.Web.API.Clients architecture

SAIF.Web.API.Clients is the client-side integration layer for the SAIF JSON Web API contract: it wraps RestSharp so a consuming application can invoke a SAIF Web API with uniform NTLM (or custom) authentication, caller-identity headers, retry-on-transient-failure, JSON serialization/deserialization, and translation of HTTP failures into typed SAIF exceptions. It depends on SAIF.Web.API.Core for the shared ApiResponse<T> envelope and SAIF.API.Exceptions.* types, and has no reference back to the server-side SAIF.Web.API package. Its source files are also shared with the SAIF.Web.API.Clients.35 sibling project, which compiles the same .cs files for a net35 target.

Responsibilities

  • Provide a single client type, JsonHttpClient, exposing GET/POST/PUT/PATCH/DELETE verb methods that route every call through one shared HTTP execution funnel (GetJsonResponse).
  • Authenticate outbound requests, defaulting to Windows NTLM (RestSharp.Authenticators.NtlmAuthenticator) when no IAuthenticator is supplied.
  • Attach caller-identity headers (Client-Name, Application-ID, Client-User) from a caller-supplied ClientInfo on every request, and optionally forward the ambient ASP.NET user's identity via an OnBehalfOfUser header (SetOnBehalfOfUserHeaders).
  • Retry transient transport/gateway failures (RetryOnStatusCodes, default {0, 401, 502, 503}) with a fixed delay before surfacing them as errors.
  • Serialize request bodies and normalize response payloads (array, object, or scalar JSON shapes) into a uniform T[]/T result via JsonSerializer and GetContentArray<T>.
  • Translate non-2xx/3xx HTTP responses into typed SAIF.API.Exceptions.* exceptions (ExceptionHandler), so consumers catch domain exceptions rather than transport or RestSharp types.
  • Normalize a forwarded Windows identity to a bare account name (HttpContextExtensions.DomainlessUser).

Component boundaries

  • JsonHttpClient: the sole public entry point. Owns per-instance immutable target configuration (_serviceBaseUri, _clientInfo, _authenticator) and mutable behavioural switches (timeout override, date-time-zone handling, retry policy, success enforcement, on-behalf-of-user forwarding). Contains the single HTTP execution funnel (GetJsonResponse) through which every public verb method passes, so header injection, authentication, retry, and success-enforcement cannot be bypassed per call.
  • JsonSerializer: an adapter implementing RestSharp.Serializers.ISerializer over Newtonsoft.Json.JsonSerializer, used to serialize request bodies with a configurable DateTimeZoneHandling.
  • ExceptionHandler: a stateless static boundary component that guards response success and maps HTTP status codes and response content to specific SAIF.API.Exceptions.* subclasses (NoMatchingDataException, UnauthorizedAccessException, InvalidDataException, InternalServerError).
  • ClientInfo: a plain, unvalidated DTO (MachineName, ApplicationId, UserId) supplied by the consuming application and copied into per-request headers.
  • JsonResponse (internal): the transport-level response envelope produced by GetJsonResponse before it is projected into the public ApiResponse<T>.
  • HttpContextExtensions.DomainlessUser: an extension method on System.Web.HttpContext that strips a DOMAIN\ prefix or @domain suffix from the ambient user's identity name.
  • EnvironmentSetting: a public, immutable key/value DTO with no consumer anywhere in the repository — dormant public surface, not part of the active component model.
  • SAIF.Web.API.Core (external boundary, project reference): supplies ApiResponse<T> and the SAIF.API.Exceptions.SAIFException hierarchy that this library projects into and throws, respectively. This library does not own or modify those types.

Interaction model

  • A consuming application constructs a single JsonHttpClient per target service (base URI, ClientInfo, optional IAuthenticator/timeout), then calls one of the seven public verb methods (GetResult<T>, GetResults<T>, GetResponse<T>, PostJson<T>, PutJson<T>, PatchJson<T>, DeleteJson<T>).
  • Every verb method delegates to the private GetJsonResponse funnel, which builds a new RestSharp.RestClient per call, assigns the authenticator, clears default content handlers (accepting only application/json), applies the configured timeout, serializes the request body via JsonSerializer, attaches identity headers, optionally attaches the OnBehalfOfUser header, executes the request synchronously (client.Execute, blocking, no async/await), and retries on a configured status-code set before evaluating success.
  • On success, the transport-level JsonResponse is projected by GetApiResponse<T> into the public ApiResponse<T> envelope, using GetContentArray<T> to normalize the JSON payload shape (array, object, or scalar) into T[].
  • On failure (when EnsureSuccessfulResponse is true, the default), ExceptionHandler.EnsureSuccessfulResponse selects the best available error text and calls ExceptionHandler.ToKnownError, which maps the HTTP status to a SAIF.Web.API.Core exception type and throws it — the funnel never returns a failed result to the caller in this mode.
  • All interaction is synchronous and stateless across calls beyond the immutable per-instance target configuration; there is no connection reuse, caching, or ambient mutable state maintained by the client between calls.
  • When SetOnBehalfOfUserHeaders is enabled, the client couples itself to an ambient ASP.NET request pipeline (System.Web.HttpContext.Current) — a leaky dependency that assumes a web host and has no guard for a non-web caller (console/service/test), where it throws NullReferenceException.

Architecture diagram

The following diagrams show the high-level component interactions within this library and its immediate dependency boundary, split by concern so each renders at a readable width.

The first diagram shows request construction and outbound transport:

flowchart TD
    App["Consumer application"] --> Client["JsonHttpClient"]
    Client --> Serializer["JsonSerializer<br>RestSharp.Serializers.<br>ISerializer)"]
    Serializer --> Newtonsoft["Newtonsoft.Json.<br>JsonSerializer"]
    Client --> Rest["RestSharp RestClient /<br>IAuthenticator"]

The second diagram shows the caller-identity context JsonHttpClient attaches to every request:

flowchart TD
    Client["JsonHttpClient"] --> Info["ClientInfo<br>(caller identity<br>headers)"]
    Client --> CtxExt["HttpContextExtensions<br>(ambient user, optional)"]

The third diagram shows response projection and failure translation:

flowchart TD
    Client["JsonHttpClient"] --> Handler["ExceptionHandler<br>(failure translation)"]
    Handler --> Exceptions["SAIF.API.Exceptions.*<br>(SAIF.Web.API.Core)"]
    Client --> JsonResp["JsonResponse (internal)"]
    JsonResp --> ApiResp["ApiResponse&lt;T&gt;<br>(SAIF.Web.API.Core)"]

Sequence diagram

The following diagram shows the primary request/response sequence for a GET call through the shared HTTP execution funnel, including the retry path.

sequenceDiagram
    participant App as Consumer application
    participant Client as JsonHttpClient
    participant Rest as RestSharp RestClient
    participant API as SAIF Web API
    App->>Client: GetResult<T>(methodUriSegment)
    Client->>Client: GetJsonResponse (build client, add headers, serialize body)
    Client->>Rest: Execute(request)
    Rest->>API: HTTP request (NTLM or custom auth)
    API-->>Rest: HTTP response
    Rest-->>Client: IRestResponse
    alt status in RetryOnStatusCodes and retries remain
        Client->>Client: Thread.Sleep(RetryDelayInMs), retry
    else EnsureSuccessfulResponse and status is failure
        Client->>Client: ExceptionHandler.ToKnownError (throws SAIFException)
    else success
        Client->>Client: GetApiResponse / GetContentArray (normalize JSON shape)
        Client-->>App: T / ApiResponse<T>
    end

Design constraints

  • No asynchronous API. All HTTP execution is synchronous and blocking (client.Execute, Thread.Sleep for retry delay); there is no async/await surface, which constrains callers on frameworks that expect non-blocking I/O.
  • Single execution funnel, no bypass. Every public verb method routes through GetJsonResponse, so header injection, authentication, retry, and success-enforcement are applied uniformly and cannot be selectively disabled per call — only globally via the client's public properties.
  • Not substitutable by interface. JsonHttpClient defines no interface and has no virtual members, so it cannot be mocked or substituted without wrapping it; the only defined interface point is the injectable IAuthenticator and the optional custom Newtonsoft.Json.JsonSerializer.
  • Ambient web-context coupling. SetOnBehalfOfUserHeaders = true binds the client to System.Web.HttpContext.Current, which is null outside an ASP.NET request — an unguarded dependency that limits reuse in console/service/test hosts.
  • Silent parse-failure masking. GetContentArray<T> swallows deserialization exceptions: for a string target type it substitutes the exception message as if it were response data, and for any other target type it silently returns an empty result — callers cannot distinguish "no data" from "a parse failure" (flagged as a defect risk — see BR-CLI-016).
  • No validation, logging, or caching. The library performs no input validation of methodUriSegment, serviceBaseUri, requestBody, or ClientInfo; no authorization decisions; no rate limiting, circuit breaking, or response caching; and no logging or auditing of any kind (verified absence).
  • Duplicated source, separate assembly. SAIF.Web.API.Clients.35 compiles the same source files via file-linking against a different target framework (net35) and older package versions, and excludes HttpContextExtensions.cs. Edits to this library's source directly affect that sibling project even though it is analyzed and documented separately.

Sources

  • src/SAIF.Web.API.Clients/JsonHttpClient.cs
  • src/SAIF.Web.API.Clients/ExceptionHandler.cs
  • src/SAIF.Web.API.Clients/JsonSerializer.cs
  • src/SAIF.Web.API.Clients/JsonResponse.cs
  • src/SAIF.Web.API.Clients/ClientInfo.cs
  • src/SAIF.Web.API.Clients/EnvironmentSetting.cs
  • src/SAIF.Web.API.Clients/Extensions/HttpContextExtensions.cs
  • src/SAIF.Web.API.Clients/SAIF.Web.API.Clients.csproj
  • src/SAIF.Web.API.Clients.35/SAIF.Web.API.Clients.35.csproj
  • src/SAIF.Web.API.Core/ApiResponse.cs
  • src/SAIF.Web.API.Core/SAIFException.cs