Example: ASP.NET Core API client consumption¶
consumption.md is the page a developer opens when they have to call a Core API
client from an application: how to reference the package, how the client is
registered and configured, 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¶
## Dependency injectionrecords the package's ownIServiceCollectionextension when one exists, and lists the registrations a consumer must still supply. The registration snippet is what a real consumer writes, not an idealized one.- Every constructor dependency the container must resolve is listed, and the
page says whether the injected
IHttpJsonClientcan be shared between clients, given that the client constructor mutates it (SetClientInfo, authentication). The risk is raised in Tech debt when the registrations found make it possible. ## Configurationnames every setting key, with its app key, and says which environments define it. Values are characterized, never reproduced: no host name, no credential. The page states when a missing key fails (at construction, not on the first call).- 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. The Sync or async row is recorded for the whole member set. - The timeout row says which case this client is: an explicit
msTimeoutOverridewith the number, or no argument at all and therefore theIHttpJsonClientdefault inherited. The row carries the dagger when it relies on package behavior. ## Error handling for consumersnames the exception type for each failure, covers the empty-body case, and points the "route matches no endpoint" row at the endpoint map's Route drift section.- Usage examples come from real consumer source. 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 ASP.NET Core 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, registers, configures, and calls the client.
status: draft
owner: Premium Systems Team
last_reviewed: 2026-10-21
---
# Consumption
A consumer references the `SAIF.Premium.Api.Client` package, calls
`services.AddPremiumClient()` in its `Startup`, makes sure the `premiumbaseurl`
and route settings exist for its environment, and injects `IPremiumClient`. The
client reads its settings and sets its transport up in its constructors, so a
missing setting fails when the container first resolves the client, not on the
first call. `SAIF.Policy.QuoteApi` is the reference consumer. The client has no
timeout, retry or cancellation of its own, and one of its six mapped members,
`AddAdjustmentAsync`, currently fails with a `404` because its route has drifted
from the API.
## Package reference
| Field | Value |
| --- | --- |
| Package id | `SAIF.Premium.Api.Client` |
| Version | `8.2026.1021.1` |
| Reference style | `<PackageReference>` |
| Target framework | `net8.0` |
```xml
<PackageReference Include="SAIF.Premium.Api.Client" Version="8.2026.1021.1" />
```
Installing the package also brings in `SAIF.NetCore.ServiceExtensions`, `SAIF.Http.ResponseValidator` and `Microsoft.Extensions.DependencyInjection.Abstractions`, and ships the `SAIF.Premium.Api.Contracts` assembly inside the package. See [Package](package.md) for what that means for the consumer.
## Dependency injection
The package ships an `IServiceCollection` extension, `AddPremiumClient()`, in
`ServiceCollectionExtensions.cs`. It registers the client and its configuration
type; the transport and the environment client come from the SAIF registration
that every `CoreAPIStartup` service already performs.
| Registration | Provided by | Lifetime | Notes |
| --- | --- | --- | --- |
| `IPremiumClient` -> `PremiumClient` | `services.AddPremiumClient()` in this package | Transient | One instance per resolution |
| `IPremiumClientConfiguration` -> `PremiumClientConfiguration` | `services.AddPremiumClient()` in this package | Transient | Reads settings in its constructor |
| `IHttpJsonClient` | SAIF .NET Core service registration (`CoreAPIStartup`)† | Not determined from source | Mutated by the client constructor (`SetClientInfo`, `SetNtlmAuthentication`) |
| `IEnvironmentClient` | SAIF .NET Core service registration (`CoreAPIStartup`)† | Not determined from source | Supplies settings and `ApplicationName` |
```csharp
// src/SAIF.Policy.QuoteApi/Startup.cs
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
services.AddPremiumClient();
}
```
The container must resolve these constructor parameters: `PremiumClient` takes
`IHttpJsonClient` and `IPremiumClientConfiguration`; `PremiumClientConfiguration`
takes `IEnvironmentClient`.
`PremiumClient`'s constructor mutates the injected `IHttpJsonClient`: it calls
`SetClientInfo(...)` and `SetNtlmAuthentication()` on it. If the SAIF
registration shares one `IHttpJsonClient` instance across clients, the last
client constructed would win. Its lifetime is not visible in the repository, so
whether this can happen is not determined from source. Today every client in the
consumer is configured from the same base URL and the same process identity, so
the shared case would be harmless, but a second client in the same host with a
different base URL would silently redirect `PremiumClient`. This is recorded in
[Tech debt](tech-debt.md).
## Configuration
| Setting key | App key | Purpose | Defined for | Consumer action |
| --- | --- | --- | --- | --- |
| `premiumbaseurl` | `PremiumApi` | Base URL of `SAIF.Premium.Api` | DEV, TEST, PROD (seeded by `environmentsettings/settings.yml`) | None - deployment-seeded |
| `premiumroute` | `PremiumApi` | Relative route for `GetPremiumAsync` | DEV, TEST, PROD | None - deployment-seeded |
| `premiumhistoryroute` | `PremiumApi` | Relative route format for `GetPremiumHistoryAsync` | DEV, TEST, PROD | None - deployment-seeded |
| `estimateroute` | `PremiumApi` | Relative route for `EstimatePremiumAsync` | DEV, TEST, PROD | None - deployment-seeded |
| `adjustmentroute` | `PremiumApi` | Relative route format for `AddAdjustmentAsync` | DEV, TEST, PROD | None - deployment-seeded; the seeded value has drifted, see [Endpoint map](endpoint-map.md#route-drift) |
| `classcoderoute` | `PremiumApi` | Relative route for `GetClassCodeRateAsync` | DEV, TEST, PROD | None - deployment-seeded |
| `ratetableroute` | `PremiumApi` | Relative route for `GetCurrentRateTableAsync` | DEV, TEST, PROD | None - deployment-seeded |
A consumer defines no setting itself in a SAIF environment, because the release
pipeline seeds all seven for every track. A key that is missing or empty makes
`PremiumClientConfiguration` throw `ArgumentNullException` in its constructor,
so the failure surfaces when the container first resolves `IPremiumClient`, not
on the first call.
## Construction
The container constructs the client; there is no `Initialize(...)` step and
every member is usable as soon as the constructor returns.
| Step | Requirement |
| --- | --- |
| Base URL | Read from setting `premiumbaseurl` by `PremiumClientConfiguration` |
| Client info | `SetClientInfo` is called by the client with the base URL, `Environment.UserName.ToUpper()`, `Environment.MachineName.ToUpper()` and `IEnvironmentClient.ApplicationName`, and no `msTimeoutOverride` |
| Authentication | `SetNtlmAuthentication()` - the consumer's process identity must be authorized by `SAIF.Premium.Api` (`PremiumReadResource`, plus `PremiumAdjustResource` for adjustments) |
## Resilience
| Concern | Finding | Configurable by consumer | Source | Tech debt |
| --- | --- | --- | --- | --- |
| Sync or async | All async - every remote member returns `Task<T>` | No | `src/SAIF.Premium.Api.Client/IPremiumClient.cs:9` | No |
| Timeout | None configured - `SetClientInfo` receives no `msTimeoutOverride`, so the `IHttpJsonClient` default applies† | No | `src/SAIF.Premium.Api.Client/PremiumClient.cs:31` | Yes |
| Retry | None configured | No | - | Yes |
| Circuit breaking | None configured | No | - | Yes |
| Cancellation | Not supported - no member accepts a `CancellationToken` | - | `src/SAIF.Premium.Api.Client/IPremiumClient.cs:9` | Yes |
| Thread safety | Not determined from source - the constructor mutates a shared `IHttpJsonClient` whose lifetime is not visible | - | `src/SAIF.Premium.Api.Client/PremiumClient.cs:31` | Yes |
The client performs no retry of any kind. A consumer that must survive an API
restart wraps its own calls, for example with a Polly policy around the
`IPremiumClient` call, and `SAIF.Policy.QuoteApi` does not today. There is no
circuit breaker either: when the Premium API is down every quote request waits
out the transport's default timeout. The timeout is not configurable and cannot
be tightened by the consumer, so an outer timeout belongs in the consumer.
Cancellation is unavailable. An ASP.NET Core consumer cannot propagate
`HttpContext.RequestAborted` into the call; a request the caller has abandoned
keeps its outbound call alive until the transport times out. Each gap is
recorded in [Tech debt](tech-debt.md).
## Error handling for consumers
| Failure | Surfaces as | Consumer action |
| --- | --- | --- |
| Missing or empty setting | `ArgumentNullException` at construction | Define the setting for the environment; this is a deployment error, not a runtime condition |
| API unreachable | `HttpRequestException`† | Retry with backoff in the consumer; treat as transient |
| Non-success status | `HttpStatusCodeException` from `HttpResponseValidator.ValidateHttpResponse`† | Inspect the status: `400`, `404` and `409` are caller errors that will not succeed on retry; `5xx` is transient |
| Route matches no endpoint | Non-success status (404) surfaced as `HttpStatusCodeException`† | See [Endpoint map](endpoint-map.md#route-drift) |
| Timeout exceeded | `TaskCanceledException`† | Treat as transient; a `POST` may still have been applied by the API |
| Empty response body | `null` returned by the typed member | Null-check the result; no member indexes into the body, so an empty body is a silent `null`, not an exception |
An adjustment fails today even when the policy exists and the caller is
authorized, because `AddAdjustmentAsync` posts to `/v1/Premiums/{policyNumber}/adjustment`
and the API serves `/adjustments`. The `404` looks like a missing policy.
## Usage examples
`SAIF.Policy.QuoteApi` prices new quotes and shows a policyholder's premium
history. It injects `IPremiumClient` with a primary constructor.
```csharp
// src/SAIF.Policy.QuoteApi/Services/QuoteService.cs
public class QuoteService(IPremiumClient premiumClient)
{
public Task<PremiumEstimate> EstimateAsync(string classCode, decimal payroll, DateTime effectiveDate) =>
premiumClient.EstimatePremiumAsync(classCode, payroll, effectiveDate);
public Task<List<PremiumTerm>> HistoryAsync(string policyNumber) =>
premiumClient.GetPremiumHistoryAsync(policyNumber);
public Task<PremiumAdjustment> AdjustAsync(string policyNumber, PremiumAdjustmentRequest request) =>
premiumClient.AddAdjustmentAsync(policyNumber, request);
}
```
| Scenario | Members used | Notes |
| --- | --- | --- |
| Price a new quote | `EstimatePremiumAsync` | Query values are formatted into the URL without encoding; `classCode` values with reserved characters would break the call |
| Show a policy's premium history | `GetPremiumHistoryAsync` | Returns terms with voided terms already excluded by the API |
| Apply a premium adjustment | `AddAdjustmentAsync` | Fails with `404` today because of route drift; see [Endpoint map](endpoint-map.md#route-drift) |
## Lifecycle guidance
| Question | Answer |
| --- | --- |
| Lifetime to register | Transient, as `AddPremiumClient()` registers it. The client holds no per-call state, but its constructor mutates the injected `IHttpJsonClient` |
| Disposable | No. `PremiumClient` does not implement `IDisposable` |
| Safe to share across threads | Not determined from source, from the Resilience table: the lifetime of the shared `IHttpJsonClient` is not visible |
## 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.Premium.Api.Client/PremiumClient.cs`
- `src/SAIF.Premium.Api.Client/IPremiumClient.cs`
- `src/SAIF.Premium.Api.Client/PremiumClientConfiguration.cs`
- `src/SAIF.Premium.Api.Client/ServiceCollectionExtensions.cs`
- `src/SAIF.Policy.QuoteApi/Startup.cs`
- `src/SAIF.Policy.QuoteApi/Services/QuoteService.cs`
- `environmentsettings/settings.yml`