Service-to-Service Interactions & API
Overview & Intent
LinkApi (encapsulated primarily in the LinkSdk project under DotNet/LinkSdk) is the official client SDK and HTTP abstraction layer for the Link Cloud platform. In a distributed healthcare data platform where microservices interact asynchronously via Kafka events and synchronously via REST APIs, LinkSdk provides a unified, strongly-typed, and resilient mechanism for invoking REST endpoints across all Link microservices.
Core Objectives
- Standardized Inter-Service Communication: Eliminates duplicate HttpClient boilerplate across backend services, orchestration tools (Automation.Link), the management UI (Automation.UI), and automated end-to-end test suites.
- Automated Security & Authentication: Automatically obtains and injects short-lived system Bearer tokens (ICreateSystemToken) into outgoing requests, while supporting development environments through anonymous authentication bypass.
Diagnostic Transparency (Non-Throwing Responses): Wraps all HTTP responses in
LinkApiResponse<T>instead of throwing exceptions on 4xx/5xx responses. This preserves HTTP status codes, request bodies, raw response content, and OpenTelemetry trace identifiers (TraceId) for diagnostics and error reporting. - Dynamic Service Discovery via Service Registry: Binds to ServiceRegistry configuration so endpoints can resolve dynamic base URLs across local Docker Compose, Kubernetes, and Azure Cloud deployments.
Architectural Design
graph TD
subgraph Consumers["API Consumers"]
A1["Automation.Link (CLI / Runner)"]
A2["Automation.UI (Web Management)"]
A3["BackendE2ETests"]
A4["Internal Microservices"]
end
subgraph DI["Dependency Injection & Configuration"]
DIReg["AddLinkSdk() (ServiceCollectionExtensions)"]
SR["IOptions<ServiceRegistry>"]
BO["IOptions<LinkBearerServiceOptions>"]
TS["IOptions<LinkTokenServiceSettings>"]
ST["ICreateSystemToken (Token Service)"]
end
subgraph Clients["Typed Service Clients"]
direction TB
C1["FacilityServiceClient\n(IFacilityServiceClient)"]
C2["CensusServiceClient\n(ICensusServiceClient)"]
C3["DataAcquisitionServiceClient\n(IDataAcquisitionServiceClient)"]
C4["NormalizationServiceClient\n(INormalizationServiceClient)"]
C5["QueryDispatchServiceClient\n(IQueryDispatchServiceClient)"]
C6["ReportServiceClient\n(IReportServiceClient)"]
C7["MeasureEvalServiceClient\n(IMeasureEvalServiceClient)"]
C8["ValidationServiceClient\n(IValidationServiceClient)"]
C9["SubmissionServiceClient\n(ISubmissionServiceClient)"]
C10["DmrpServiceClient\n(IDmrpServiceClient)"]
C11["AdminBffIntegrationClient\n(IAdminBffIntegrationClient)"]
end
subgraph Core["Core Base Layer (LinkApiClientBase)"]
Flurl["FlurlClient Engine\n(Thread-Safe Singleton)"]
AuthHook["BeforeCall Hook\n(Bearer JWT Injection)"]
JsonConf["JsonSerializerOptions\n(Case-Insensitive, String Enums)"]
ExecEng["Execution Engine\n(SendAsync, SendStringAsync, SendBytesAsync)"]
TraceEng["Trace Correlation\n(traceparent / X-Trace-Id)"]
end
subgraph Output["Response Abstraction"]
Resp["LinkApiResponse<T> / LinkApiResponse"]
RespFields["- StatusCode (int)\n- IsSuccessStatusCode (bool)\n- Body (T)\n- RawBody (string)\n- ContentType (string)\n- TraceId (string)\n- RequestUrl / Method / Body"]
end
subgraph TargetServices["Link Backend Microservices"]
S1["Tenant / Facility Service (.NET)"]
S2["Census Service (.NET)"]
S3["Data Acquisition Service (.NET)"]
S4["Normalization Service (.NET)"]
S5["Query Dispatch Service (.NET)"]
S6["Report Service (.NET)"]
S7["Measure Evaluation Service (Java)"]
S8["Validation Service (Java)"]
S9["Submission Service (.NET)"]
S10["DMRP Service (.NET)"]
S11["Admin BFF Service (.NET)"]
end
%% Consumer to DI / Clients
Consumers --> DIReg
DIReg --> Clients
SR -.-> Clients
BO -.-> Clients
TS -.-> Clients
ST -.-> Clients
%% Clients to Base
Clients --> Core
Core --> Flurl
AuthHook --> ST
Flurl --> TargetServices
TargetServices --> ExecEng
ExecEng --> TraceEng
ExecEng --> Output
Output --> Consumers
Base Client Foundation (LinkApiClientBase)
All service clients inherit from LinkApiClientBase, which manages:
- Flurl HTTP Client Lifecycle: Initializes a thread-safe FlurlClient instance per service base URL, configuring JSON serialization with case-insensitivity and string-based enum converters (JsonStringEnumConverter).
- Dynamic Per-Request Authentication: Intercepts outgoing requests using BeforeCall hooks to generate or refresh JWT tokens using ICreateSystemToken and the configured SigningKey.
- Execution Pipelines: Exposes protected methods SendAsync
(), SendAsync(), SendStringAsync(), and SendBytesAsync() that intercept exceptions (FlurlHttpException) and return safe LinkApiResponse objects. - Telemetry Correlation: Extracts distributed trace IDs from traceparent (W3C standard) or X-Trace-Id headers on responses to facilitate end-to-end log correlation in Grafana/Loki.
Structured Response Model (LinkApiResponse<T>)
Rather than relying on EnsureSuccessStatusCode() which conceals failure context, LinkSdk utilizes LinkApiResponse
- StatusCode: The exact HTTP integer status code (e.g., 200, 201, 204, 400, 404, 500).
- IsSuccessStatusCode: Boolean helper verifying
StatusCode gte 200 && StatusCode lt 300 - Body: Strongly-typed deserialized response model (T).
- RawBody: Raw response string for diagnostics when deserialization is skipped or an error is encountered.
- ContentType: Media type of the response (e.g., application/json, application/pdf).
- RequestUrl, RequestMethod, RequestBody: Context regarding the outgoing request.
- TraceId: Extracted trace identifier correlating request and server logs.
Service Client Inventory
LinkSdk registers typed interfaces for each domain boundary in the system:
LinkSdk Service Client Inventory
| Client Name | Interface | Target Service Domain |
|---|---|---|
FacilityServiceClient |
IFacilityServiceClient |
Tenant, Facility, Vendor, and VendorVersion management |
CensusServiceClient |
ICensusServiceClient |
Patient census, facility lists, and scheduling |
DataAcquisitionServiceClient |
IDataAcquisitionServiceClient |
FHIR query configurations, query plans, and acquisition logs |
NormalizationServiceClient |
INormalizationServiceClient |
Normalization rules, operations, and resource transform configs |
QueryDispatchServiceClient |
IQueryDispatchServiceClient |
Query dispatch triggers and schedule management |
ReportServiceClient |
IReportServiceClient |
Report schedules, generation requests, and report tracking |
MeasureEvalServiceClient |
IMeasureEvalServiceClient |
Measure definition uploads and CQL evaluations (Java service) |
ValidationServiceClient |
IValidationServiceClient |
FHIR validation profiles, categories, and rule initializations |
SubmissionServiceClient |
ISubmissionServiceClient |
Submission configurations, bundles, and submission manifests |
DmrpServiceClient |
IDmrpServiceClient |
Direct Message / DMRP configurations and submissions |
AdminBffIntegrationClient |
IAdminBffIntegrationClient |
Admin Backend-for-Frontend aggregation endpoints |
Configuration & Dependency Injection
Configuration Settings (appsettings.json)
LinkSdk requires ServiceRegistry, LinkTokenServiceSettings, and Authentication configurations:
{
"ServiceRegistry": {
"TenantServiceApiUrl": "http://tenant-service:8080",
"CensusServiceApiUrl": "http://census-service:8080",
"DataAcquisitionServiceApiUrl": "http://data-acquisition-service:8080",
"NormalizationServiceApiUrl": "http://normalization-service:8080",
"QueryDispatchServiceApiUrl": "http://query-dispatch-service:8080",
"ReportServiceApiUrl": "http://report-service:8080",
"MeasureServiceApiUrl": "http://measure-eval-service:8080",
"ValidationServiceApiUrl": "http://validation-service:8080",
"SubmissionServiceApiUrl": "http://submission-service:8080",
"DmrpServiceApiUrl": "http://dmrp-service:8080",
"AdminBffApiUrl": "http://admin-bff:8080"
},
"LinkTokenService": {
"SigningKey": "YourSecureJwtSigningKeyString"
},
"Authentication": {
"AllowAnonymous": false
}
}
Service Registration
All SDK clients are registered as thread-safe Singletons using AddLinkSdk():
using LantanaGroup.Link.Sdk.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register configuration and token generation
builder.Services.Configure<ServiceRegistry>(builder.Configuration.GetSection("ServiceRegistry"));
builder.Services.Configure<LinkTokenServiceSettings>(builder.Configuration.GetSection("LinkTokenService"));
builder.Services.Configure<BackendAuthenticationServiceExtension.LinkBearerServiceOptions>(
builder.Configuration.GetSection("Authentication"));
builder.Services.AddSingleton<ICreateSystemToken, CreateSystemTokenService>();
// Register all Link SDK API clients
builder.Services.AddLinkSdk();
Example Usages
Example 1: Querying and Creating a Facility (IFacilityServiceClient)
public class FacilityServiceManager
{
private readonly IFacilityServiceClient _facilityClient;
private readonly ILogger<FacilityServiceManager> _logger;
public FacilityServiceManager(IFacilityServiceClient facilityClient, ILogger<FacilityServiceManager> logger)
{
_facilityClient = facilityClient;
_logger = logger;
}
public async Task<FacilityModel?> GetOrCreateFacilityAsync(string facilityId, FacilityModel model, CancellationToken ct)
{
var getResponse = await _facilityClient.GetAsync(facilityId, ct);
if (getResponse.IsSuccessStatusCode && getResponse.Body != null)
{
return getResponse.Body;
}
if (getResponse.StatusCode == 404)
{
_logger.LogInformation("Facility {FacilityId} not found. Creating...", facilityId);
var createResponse = await _facilityClient.CreateAsync(model, ct);
if (createResponse.IsSuccessStatusCode)
{
return createResponse.Body;
}
_logger.LogError("Failed to create facility. Status: {Status}, TraceId: {TraceId}, Body: {RawBody}",
createResponse.StatusCode, createResponse.TraceId, createResponse.RawBody);
}
return null;
}
}
Example 2: Managing FHIR Query Configurations (IDataAcquisitionServiceClient)
public class DataAcquisitionOrchestrator
{
private readonly IDataAcquisitionServiceClient _dataAcqClient;
public DataAcquisitionOrchestrator(IDataAcquisitionServiceClient dataAcqClient)
{
_dataAcqClient = dataAcqClient;
}
public async Task ConfigureFhirQueryAsync(CreateFhirQueryConfigurationRequestApiModel configRequest, CancellationToken ct)
{
var response = await _dataAcqClient.CreateFhirQueryConfigurationAsync(configRequest, ct);
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"Failed to configure FHIR query. Status: {response.StatusCode}, RequestUrl: {response.RequestUrl}, Error: {response.RawBody}");
}
}
}
Example 3: Initializing Validation Artifacts with Diagnostics (IValidationServiceClient)
public class ValidationInitializer
{
private readonly IValidationServiceClient _validationClient;
private readonly ILogger<ValidationInitializer> _logger;
public ValidationInitializer(IValidationServiceClient validationClient, ILogger<ValidationInitializer> logger)
{
_validationClient = validationClient;
_logger = logger;
}
public async Task InitializeAsync(CancellationToken ct)
{
var existingArtifacts = await _validationClient.GetArtifactsAsync(ct);
if (existingArtifacts.IsSuccessStatusCode && existingArtifacts.Body?.Count > 0)
{
_logger.LogInformation("Validation artifacts are already initialized.");
return;
}
var initResponse = await _validationClient.InitializeArtifactsAsync(ct);
if (!initResponse.IsSuccessStatusCode)
{
_logger.LogError("Validation initialization failed with status {Code} (Trace ID: {TraceId})",
initResponse.StatusCode, initResponse.TraceId);
}
}
}
Relationships
flowchart LR nservice_to_service_api_25A99A96["Design: Service-to-Service Interactions & API"]