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&lt;ServiceRegistry&gt;"]
        BO["IOptions&lt;LinkBearerServiceOptions&gt;"]
        TS["IOptions&lt;LinkTokenServiceSettings&gt;"]
        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&lt;T&gt; / 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 containing:

  • 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"]