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 |
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 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 |
// 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.
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 |
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.
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 |
| 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.
// 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 |
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
- Overview
- Architecture
- Package
- Client reference
- Endpoint map
- Contracts
- Dependencies
- Business rules
- Tech debt
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.cssrc/SAIF.Premium.Api.Client/IPremiumClient.cssrc/SAIF.Premium.Api.Client/PremiumClientConfiguration.cssrc/SAIF.Premium.Api.Client/ServiceCollectionExtensions.cssrc/SAIF.Policy.QuoteApi/Startup.cssrc/SAIF.Policy.QuoteApi/Services/QuoteService.csenvironmentsettings/settings.yml