Example: API client consumption¶
consumption.md is the page a developer opens when they have to call the client
from an application: how to reference the package, how to construct the client,
what happens when the API misbehaves, and what to do about it. Doc gen produces
one per client at docs/clients/<client-slug>/consumption.md.
What reviewers should check¶
- The
## Constructionsnippet is the construction a consumer actually performs, including whereClientInfoand the base URL come from. When the client uses a parameterless constructor plusInitialize(...), the two-step form is shown and the page says remote members fail beforeInitializeruns. - Base URL provenance is a named config key or constant, not "configuration". A reader must be able to find the setting.
- Every row of
## Resilienceis present. "None configured" is an explicit, citable finding with a source line and a tech-debt flag — never a blank cell and never a dropped row. Retry in particular is "None configured" across every surveyed SAIF client. - The timeout row says which case this client is: a default passed into
JsonHttpClientwith the number and the source line, or no timeout argument at all and therefore the transport default inherited. ## Error handling for consumerscovers the empty-result case and names where aResults[0]index would throw rather than return null.- Usage examples come from real consumer source or the client's own tests. An invented calling pattern no caller performs is the defect this section exists to prevent.
Example¶
The document below is a complete, correct client-consumption.md for a
fictional API client package.
View rendered example — the same document as a real page, so you can compare rendering as well as source.
---
title: Consumption
summary: How a consuming application references, constructs, and calls the client.
status: draft
owner: Agency Systems Team
last_reviewed: 2026-09-10
---
# Consumption
A consumer references the `SAIF.Agency.Api.Client` package, supplies a
`ClientInfo` identifying the calling application and the base URL of the target
environment's API, and constructs an `AgencyClient`. There is no initialization
step and no ambient configuration: everything the client needs arrives through
its constructor, which is why the same process can hold clients pointed at two
environments. `SAIF.Policy.RenewalNotice` is the reference consumer and
registers the client with Autofac during `RegisterAppDependencies`.
## Package reference
| Field | Value |
| --- | --- |
| Package id | `SAIF.Agency.Api.Client` |
| Version | `3.2.0` |
| Reference style | `packages.config` |
| Target framework | .NET Framework 4.8 |
Installing the package also installs the client and contracts **source** under `src\`. See [Package](package.md) for what that means for the consumer.
## Construction
```csharp
var info = new ClientInfo
{
ApplicationId = "SAIF.Policy.RenewalNotice",
MachineName = Environment.MachineName.ToUpper(),
UserId = Environment.UserName.ToUpper()
};
var baseUrl = ConfigurationManager.AppSettings["AgencyApi.BaseUrl"];
var client = new AgencyClient(info, baseUrl);
```
The base URL is the environment-specific root including the version prefix, for
example `https://api.saif.com/agency/v1/`. The trailing slash matters: every
member composes a relative URL such as `agencies/{agencyNumber}`, so a base URL
without it drops the `v1` segment and produces a `404` that looks like a missing
agency.
Credentials are not passed explicitly. `JsonHttpClient` attaches the calling
process's Windows identity through `UseDefaultCredentials`,† and the API's
Windows authentication maps that identity to an authorized service account. A
console application therefore reaches the API as whatever account the scheduled
task runs under — for `SAIF.Policy.RenewalNotice` that is `SAIF\svc-policy`.
`ClientInfo.UserId` is diagnostic only; it is logged by the API but is not the
authenticated principal.
| Step | Requirement |
| --- | --- |
| Base URL | `AgencyApi.BaseUrl` app setting in the consumer's `App.config` or `Web.config`, including the `v1/` suffix and its trailing slash |
| `ClientInfo` | Supplied by the consumer; `ApplicationId` must be the consumer's assembly name so API request logs attribute the call correctly |
| Initialization | Single-constructor; no `Initialize(...)` call exists and remote members are usable immediately |
### Dependency injection
`SAIF.Policy.RenewalNotice` registers the client in `RegisterAppDependencies`,
the Autofac hook its `SAIF.Framework.Windows` runner calls before `Initialize`.
The registration is a lambda because the constructor takes primitives that
Autofac cannot resolve by type.
```csharp
builder.Register(c => new AgencyClient(
new ClientInfo
{
ApplicationId = "SAIF.Policy.RenewalNotice",
MachineName = Environment.MachineName.ToUpper(),
UserId = Environment.UserName.ToUpper()
},
ConfigurationManager.AppSettings["AgencyApi.BaseUrl"]))
.As<IAgencyClient>()
.SingleInstance();
```
| Registration | Lifetime | Notes |
| --- | --- | --- |
| `IAgencyClient` → `AgencyClient` | SingleInstance | One instance per process; the client holds a single `JsonHttpClient` and carries no per-call state |
Web consumers register the same way but with `InstancePerLifetimeScope`, which
is the surveyed SAIF web pattern rather than a requirement of this client.
## Resilience
| Concern | Finding | Configurable by consumer | Source | Tech debt |
| --- | --- | --- | --- | --- |
| Timeout | 30 seconds default (`clientTimeoutSeconds = 30`, converted to milliseconds before it is passed to `JsonHttpClient`) | Yes (`clientTimeoutSeconds` on the three-argument constructor) | `src\SAIF.Agency.Api.Client\AgencyClient.cs:34` | No |
| Retry | None configured | No | — | Yes |
| Circuit breaking | None configured | No | — | Yes |
| Thread safety | Safe for concurrent use — the `JsonHttpClient` is assigned to a readonly field in the constructor and no member mutates client state | — | `src\SAIF.Agency.Api.Client\AgencyClient.cs:28` | No |
| Cancellation | Not supported — no member accepts a `CancellationToken` and every member is synchronous | — | `src\SAIF.Agency.Api.Client\IAgencyClient.cs:12` | Yes |
The client performs no retry of any kind, so a consumer that must survive an API
restart wraps its own calls. `SAIF.Policy.RenewalNotice` does this: agent
resolution is attempted three times with a two-second pause before the notice is
marked failed and the run continues. Nothing in the client encourages that
pattern, and a consumer that does not implement it fails the whole batch on a
single transient `503`.
There is likewise no circuit breaker. When the agency database is down, every
notice in the batch waits the full 30-second timeout before failing, which turns
a 20-minute run into one that exceeds its scheduling window. The practical
mitigation available to a consumer today is to abandon the run after a
consecutive-failure count of its own choosing.
Cancellation is unavailable because the surface is synchronous. A consumer
cannot abort an in-flight call; the 30-second timeout is the only bound. Both
the retry gap and the missing cancellation support are recorded in
[Tech debt](tech-debt.md).
## Error handling for consumers
| Failure | Surfaces as | Consumer action |
| --- | --- | --- |
| API unreachable | `ApiClientException` wrapping a `WebException`† | Retry with backoff; treat as transient and do not mark the record permanently failed |
| Non-success status | `ApiClientException` carrying the status code and response body† | Inspect the status: `400` and `409` are caller errors that will not succeed on retry; `5xx` is transient |
| Timeout exceeded | `TaskCanceledException`† | Treat as transient; note that the request may still have been applied for `UpdateAgencyContact` |
| Empty result set | `SearchAgencies` and `GetAgentsForAgency` return an empty `IList<T>`; `GetAgency` returns `null`; `UpdateAgencyContact` returns `null` from `Results.FirstOrDefault()` | Test `Count` for the list-returning members and null-check the two entity-returning members. No member indexes `Results[0]`, so no empty envelope throws here — the consequence is a silent `null`, not an exception |
## Usage examples
`SAIF.Policy.RenewalNotice` resolves the servicing agent for every notice it
generates. It reads the agency number from the policy, fetches the agents at
that agency once, and caches the result for the remainder of the run.
```mermaid
sequenceDiagram
participant RN as SAIF.Policy.RenewalNotice
participant C as AgencyClient
participant J as JsonHttpClient
participant API as SAIF.Agency.Api
RN->>C: GetAgency(agencyNumber)
C->>J: GetResult<Agency>("agencies/A-1042")
J->>API: GET v1/agencies/A-1042
API-->>J: 200 Agency
J-->>C: Agency
C-->>RN: Agency
RN->>C: GetAgentsForAgency(agencyNumber)
C->>J: GetResults<Agent>("agencies/A-1042/agents")
J->>API: GET v1/agencies/A-1042/agents
API-->>J: 200 Agent[]
J-->>C: IList<Agent>
C-->>RN: IList<Agent>
```
```csharp
// Resolve the servicing agent for a policy's agency, once per agency per run.
private Agent ResolveServicingAgent(string agencyNumber)
{
IList<Agent> agents;
if (!_agentsByAgency.TryGetValue(agencyNumber, out agents))
{
var agency = _agencyClient.GetAgency(agencyNumber);
if (agency == null)
{
throw new DataErrorException(
string.Format("Agency {0} is not known to the agency API.", agencyNumber));
}
agents = _agencyClient.GetAgentsForAgency(agencyNumber);
_agentsByAgency.Add(agencyNumber, agents);
}
return agents.FirstOrDefault(a => a.IsServicingAgent);
}
```
```csharp
// Find the active agencies for a producer code before queueing notices.
var agencies = _agencyClient.SearchAgencies("PRD-338", activeOnly: true);
foreach (var summary in agencies)
{
Log.InfoFormat("Queueing notices for {0} ({1})", summary.Name, summary.AgencyNumber);
}
```
```csharp
// Correct a returned-mail address on an agency contact record.
var stored = _agencyClient.UpdateAgencyContact(
"A-1042",
new AgencyContactUpdate
{
AddressLine1 = "400 High St SE",
AddressLine2 = null,
City = "Salem",
State = "OR",
PostalCode = "97312",
Phone = "5035551212",
Email = "service@example-agency.com"
});
Log.InfoFormat("Contact stored as {0}, {1} {2}", stored.City, stored.State, stored.PostalCode);
```
| Scenario | Members used | Notes |
| --- | --- | --- |
| Resolve the servicing agent for a renewal notice | `GetAgency`, `GetAgentsForAgency` | The reference consumer path; a missing agency raises `DataErrorException`, which the runner maps to exit code 3 |
| List active agencies for a producer code | `SearchAgencies` | Used by the agency portal's producer screen; results are capped at 200 by the API and are not paginated |
| Correct an agency contact record | `UpdateAgencyContact` | Replacement semantics — omitted properties clear the stored value, so the caller sends the complete record |
## Lifecycle guidance
| Question | Answer |
| --- | --- |
| Construct once or per call | Once per process. `SAIF.Policy.RenewalNotice` registers it `SingleInstance` and the client holds no per-call state |
| Disposable | No. `AgencyClient` does not implement `IDisposable`, and the `JsonHttpClient` it owns is never disposed for the life of the process |
| Safe to share across threads | Yes, from the Resilience table: the transport is assigned once in the constructor and no member mutates client state |
## Related documentation
- [Client index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Package](package.md)
- [Client reference](reference.md)
- [Endpoint map](endpoint-map.md)
- [Contracts](contracts.md)
- [Dependencies](dependencies.md)
- [Business rules](business-rules.md)
- [Tech debt](tech-debt.md)
> **Note:** Statements marked with † were retrieved from dependency library documentation
> via the Legends CLI RAG service. They describe inherited behavior from the indicated
> origin library and should be verified if the dependency version changes.
## Sources
- `src/SAIF.Agency.Api.Client/AgencyClient.cs`
- `src/SAIF.Agency.Api.Client/IAgencyClient.cs`
- `src/SAIF.Policy.RenewalNotice/Program.cs`
- `src/SAIF.Policy.RenewalNotice/RenewalNoticeProcessor.cs`
- `src/SAIF.Policy.RenewalNotice/App.config`