Skip to content

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
<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 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
  • 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.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