---
title: SAIF.NetCore.HttpClients business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: Framework
last_reviewed: 2026-10-02
library: SAIF.NetCore.HttpClients
reference_type: package
---

# SAIF.NetCore.HttpClients business rules

This page captures business rules enforced by `SAIF.NetCore.HttpClients` in building, authenticating, executing, and interpreting outbound JSON HTTP requests.

## BR-HTTP-01 Client initialization required before any request

| Field | Value |
| --- | --- |
| **Statement** | Every verb method requires both `ClientInfo` and a base URI to have been set; otherwise a `ClientNotInitializedException` ("Client info and base URL not initialized.") is thrown before any network call is attempted. |
| **Trigger** | Any call to `Delete`/`Get`/`Patch`/`Post`/`Put` or their async equivalents. |
| **Enforcement** | `HttpJsonClient.ValidateInitialization` |
| **Outcome** | The request is rejected locally with `ClientNotInitializedException`; no HTTP call is made. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-02 Base URL required in the two-argument constructor

| Field | Value |
| --- | --- |
| **Statement** | `baseUrl` must not be null in the `(baseUrl, ClientInfo)` constructor, which throws `ArgumentNullException` when it is; the `clientInfo` parameter on this overload is **not** null-checked. |
| **Trigger** | Construction via `new HttpJsonClient(baseUrl, clientInfo)`. |
| **Enforcement** | `HttpJsonClient` constructor |
| **Outcome** | A null base URL fails fast with `ArgumentNullException`; a null `clientInfo` is accepted and surfaces later as `ClientNotInitializedException` (see [BR-HTTP-01](#br-http-01-client-initialization-required-before-any-request)). |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-03 ClientInfo requires non-null identity values

| Field | Value |
| --- | --- |
| **Statement** | `ClientInfo` requires a non-null machine name, application ID, and user name; each throws `ArgumentNullException` individually when null. |
| **Trigger** | Construction of a `ClientInfo` instance. |
| **Enforcement** | `ClientInfo` constructor |
| **Outcome** | Construction fails immediately with `ArgumentNullException` naming the missing value. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/ClientInfo.cs` |

---

## BR-HTTP-04 Retry on configured response status codes

| Field | Value |
| --- | --- |
| **Statement** | Responses whose status is in `RetryOnStatusCodes` (default 401 Unauthorized, 502 BadGateway, 503 ServiceUnavailable) are retried up to 3 additional times, for up to 4 attempts in total. |
| **Trigger** | A response status matching the configured retry set, with retries remaining. |
| **Enforcement** | `HttpJsonClient.ExecuteRestCall` |
| **Outcome** | The request is resubmitted (new `RestClient` and request) until the status is no longer retryable or attempts are exhausted. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-05 Delay between retry attempts

| Field | Value |
| --- | --- |
| **Statement** | Each retry waits `RetryDelayInMs` (default 1000 ms) using a blocking `Thread.Sleep` before resubmitting. |
| **Trigger** | A retryable response status with retries remaining (see [BR-HTTP-04](#br-http-04-retry-on-configured-response-status-codes)). |
| **Enforcement** | `HttpJsonClient.ExecuteRestCall` |
| **Outcome** | The calling thread (or a thread-pool thread for async calls) blocks for the configured delay between attempts. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-06 Timeout override bounds

| Field | Value |
| --- | --- |
| **Statement** | A timeout override takes effect only when the value passed to `SetClientInfo` is greater than 0 and at most 600000 ms (10 minutes). A value greater than 0 replaces the stored override; a value of 0 or less leaves any previously stored override unchanged (it does not reset the client to the RestSharp default). `CreateClient` applies the stored override only when it is greater than 0 and at most 600000 ms, and falls back to the RestSharp default otherwise. |
| **Trigger** | `SetClientInfo(baseUrl, userName, machineName, applicationId, msTimeoutOverride)` and client creation for each request. |
| **Enforcement** | `HttpJsonClient.SetClientInfo`; `HttpJsonClient.CreateClient` |
| **Outcome** | A non-positive override on a later `SetClientInfo` call does not clear a previously stored positive override; the RestSharp default is used only when no valid override has ever been stored, or when the stored override is outside the 1–600000 ms range at request time. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-07 Custom headers cleared after each request unless persisted

| Field | Value |
| --- | --- |
| **Statement** | Custom entries in the `Headers` dictionary are cleared after each request is built unless `HttpClientOptions.PersistHeadersAfterRequest` is `true` (default `false`). |
| **Trigger** | Every call to `CreateRequest`, including each retry attempt. |
| **Enforcement** | `HttpJsonClient.CreateRequest` |
| **Outcome** | With default options, custom headers (including a Basic `Authorization` header) are present on attempt 1 only and absent from retries; with persistence enabled, they remain on the instance for every subsequent request. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-08 Bearer token header

| Field | Value |
| --- | --- |
| **Statement** | When the `BearerToken` property is non-empty, an `Authorization` header built from the token is added to every request. |
| **Trigger** | `CreateRequest` for any request issued while `BearerToken` is set. |
| **Enforcement** | `HttpJsonClient.CreateRequest` |
| **Outcome** | The bearer token is attached to the outgoing request; because it is a property rather than a `Headers` entry, it is never cleared by [BR-HTTP-07](#br-http-07-custom-headers-cleared-after-each-request-unless-persisted) and is resent on every retry. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-09 Basic authentication header encoding

| Field | Value |
| --- | --- |
| **Statement** | `SetBasicAuthentication(userName, password)` adds an `Authorization` header whose value is `Basic ` followed by the Base64 encoding of `user:password`. |
| **Trigger** | A call to `SetBasicAuthentication`. |
| **Enforcement** | `HttpJsonClient.SetBasicAuthentication`; `HttpJsonClient.EncodeForBasicAuthentication` |
| **Outcome** | The `Headers` dictionary gains a Basic `Authorization` entry, subject to the clearing behavior in [BR-HTTP-07](#br-http-07-custom-headers-cleared-after-each-request-unless-persisted). |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-10 NTLM authentication, explicit or current identity

| Field | Value |
| --- | --- |
| **Statement** | `SetNtlmAuthentication(userName, password)` configures NTLM with explicit credentials, and the parameterless `SetNtlmAuthentication()` configures NTLM using the current process identity; the resulting authenticator is assigned to every `RestClient` created for a request. |
| **Trigger** | A call to either `SetNtlmAuthentication` overload, followed by any request. |
| **Enforcement** | `HttpJsonClient.SetNtlmAuthentication`; `HttpJsonClient.CreateClient` |
| **Outcome** | Every request (including retries) performs NTLM authentication using the configured credential source. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-11 Serializer selection

| Field | Value |
| --- | --- |
| **Statement** | When `HttpClientOptions.Serializer` is `NewtonsoftJson`, the RestSharp client uses the Newtonsoft serializer; otherwise (`Default`) it uses RestSharp's built-in `JsonDeserializer` for `application/json`. This selection governs RestSharp's own internal handling only; it does not affect how this library converts results. |
| **Trigger** | `CreateClient` for every request. |
| **Enforcement** | `HttpJsonClient.CreateClient` |
| **Outcome** | `CreateClient` configures the RestSharp client's handler/serializer for the chosen `Serializer` value, but `ExecuteRestCall` calls the non-generic `client.Execute(request)` and `CreateRequestResult` copies `response.Content` as a raw string — RestSharp's configured serializer never parses the response. Typed conversion via `GetResult<T>`/`GetContent<T>` always follows `ToReturnType<T>` ([OP-05](operations/consume-result.md)), which uses `Convert.ChangeType` for value types/`string` and `JsonConvert.DeserializeObject<T>` (Newtonsoft) for all other types, independently of `HttpClientOptions.Serializer`. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-12 Caller-identity headers on every request

| Field | Value |
| --- | --- |
| **Statement** | Every request carries three caller-identity headers: `Client-Name` (machine name), `Application-ID` (application ID), and `Client-User` (user name), sourced from the configured `ClientInfo`. |
| **Trigger** | `CreateRequest` for every request. |
| **Enforcement** | `HttpJsonClient.CreateRequest` |
| **Outcome** | The receiving service observes caller-asserted identity headers on every call; these are unauthenticated assertions, not signed claims. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-13 Null response handling

| Field | Value |
| --- | --- |
| **Statement** | A null RestSharp response is handled defensively in `CreateRequestResult`, producing an `IHttpRequestResult` with `IsSuccessful = false` and content `"Rest response was null"`. In the current call path, `ExecuteRestCall` reads `response.StatusCode` immediately after `client.Execute(request)` and before any null check, so a null response throws `NullReferenceException` there instead of reaching `CreateRequestResult`; this branch is therefore effectively unreachable through the public verb methods as currently implemented*. |
| **Trigger** | RestSharp returns a null `IRestResponse` for a request. |
| **Enforcement** | `HttpJsonClient.CreateRequestResult` |
| **Outcome** | The defensive null-handling code exists in `CreateRequestResult`, but callers going through the public verb methods receive a `NullReferenceException` rather than this well-formed failure result if RestSharp ever returns null*. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-14 Response date parsing

| Field | Value |
| --- | --- |
| **Statement** | `ResponseDate` is parsed from the response's `date` header using a case-insensitive `SingleOrDefault` match followed by `DateTime.TryParse`. The result is `null` when the header is missing, is not a string value, or cannot be parsed. If more than one header named `date` (case-insensitive) is present, `SingleOrDefault` throws `InvalidOperationException` before parsing is attempted. |
| **Trigger** | Result construction after every request. |
| **Enforcement** | `HttpJsonClient.GetDateHeader` |
| **Outcome** | `IHttpRequestResult.ResponseDate` is populated only when exactly one `date` header is present and it is a parseable string; a duplicate `date` header aborts result construction with `InvalidOperationException` instead of yielding a null `ResponseDate`. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-15 Error message derivation order

| Field | Value |
| --- | --- |
| **Statement** | `ErrorMessage` is derived in order: (1) RestSharp's own `ErrorMessage`; (2) top-level JSON tokens `errormessage`/`exceptionmessage`/`message` joined by newline; (3) on JSON-parse failure, the raw response content; (4) if still empty, `"{code} - {description}"`. The empty-token-match fallback to raw content is inferred from an empty-sequence `Aggregate` exception being caught.* |
| **Trigger** | Result construction for an unsuccessful response. |
| **Enforcement** | `HttpJsonClient.PopulateRequestResultErrorContent` |
| **Outcome** | Callers always receive a non-empty `ErrorMessage` for failed requests, in the priority order above. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-16 Duplicate Authorization key guard on Basic authentication

| Field | Value |
| --- | --- |
| **Statement** | `SetBasicAuthentication` throws `ArgumentException` if an `Authorization` key already exists in `Headers` — for example when headers are persisted from a prior call.* |
| **Trigger** | A second call to `SetBasicAuthentication` on an instance with `PersistHeadersAfterRequest = true` and an existing `Authorization` header. |
| **Enforcement** | `HttpJsonClient.SetBasicAuthentication` (`Headers.Add`) |
| **Outcome** | Rotating Basic credentials on a persisted instance fails until the existing `Authorization` entry is removed manually. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-17 Error exception populated only for 4xx

| Field | Value |
| --- | --- |
| **Statement** | `ErrorException` is populated only when the response status is in the 4xx range. |
| **Trigger** | Result construction for a 4xx response. |
| **Enforcement** | `HttpJsonClient.PopulateRequestResultErrorContent` |
| **Outcome** | Non-4xx failures (for example 5xx) do not populate `ErrorException`, only `ErrorMessage`/`Content`. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## BR-HTTP-18 GetResult returns default on failure

| Field | Value |
| --- | --- |
| **Statement** | `GetResult<T>()` returns `default(T)` when `IsSuccessful` is `false`, instead of attempting to convert the response content. |
| **Trigger** | A caller invokes `GetResult<T>()` or `GetResult<T>(CultureInfo)` on an unsuccessful result. |
| **Enforcement** | `HttpRequestResult.GetResult<T>` |
| **Outcome** | Callers never receive a converted value derived from a failed response's content through `GetResult<T>`. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpRequestResult.cs` |

---

## BR-HTTP-19 Content conversion trims quotes and honors culture

| Field | Value |
| --- | --- |
| **Statement** | `ToReturnType<T>` strips exactly one leading and trailing double-quote pair, preserving any inner escaped quotes, only when `T` is `string` and the content both starts and ends with `"`; the unwrapped (or, for all other cases, unmodified) content is then converted with `Convert.ChangeType` using the supplied culture when `T` is a value type or `string`, and through JSON deserialization using the culture in the serializer settings for all other types. A quoted numeric string (for example `"5"` converting to `int`) is not unquoted by this path, because the quote-stripping condition only matches when `T` is `string`. |
| **Trigger** | A caller invokes `GetContent<T>()`/`GetContent<T>(CultureInfo)` or the extension method `ToReturnType<T>` directly. |
| **Enforcement** | `HttpJsonClientExtensions.ToReturnType<T>` |
| **Outcome** | Content is converted according to its target type and the requested culture, regardless of the response's success status. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClientExtensions.cs` |

---

## BR-HTTP-20 Case-insensitive header lookup

| Field | Value |
| --- | --- |
| **Statement** | `GetHeaderValue<T>` performs a case-insensitive lookup by header name, returns the default value when the match is null, and unwraps `Nullable<T>` to its underlying type for conversion. |
| **Trigger** | A caller invokes `IHttpRequestResult.GetHeaderValue<T>(name)` or the underlying extension method. |
| **Enforcement** | `HttpJsonClientExtensions.GetHeaderValue<T>` |
| **Outcome** | Header lookups succeed regardless of header-name casing; a case-variant duplicate causes the lookup to throw. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClientExtensions.cs` |

---

## BR-HTTP-21 Generic async verb methods are disabled

| Field | Value |
| --- | --- |
| **Statement** | The generic `*Async<T>` verb method overloads are marked `[Obsolete("This method not thread safe and has been depracated. Please use the overload <Verb>Async(string urlPart, object body).", true)]`, making them a compile-time error to call. |
| **Trigger** | Any attempt to call `DeleteAsync<T>`, `GetAsync<T>`, `PatchAsync<T>`, `PostAsync<T>`, or `PutAsync<T>`. |
| **Enforcement** | `HttpJsonClient` class declarations (`[Obsolete]` attribute) |
| **Outcome** | Callers must use the non-generic async verb methods together with `HttpRequestResult.GetResult<T>`/`GetContent<T>` instead. |
| **Source** | `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs` |

---

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources
- `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClient.cs`
- `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpRequestResult.cs`
- `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/HttpJsonClient/HttpJsonClientExtensions.cs`
- `src/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/SAIF.NetCore.HttpClients/ClientInfo.cs`
