Appearance
Inter-Service Communication
Microtec microservices communicate with each other using typed HTTP clients generated from the Microtec.PublicApi.* NuGet packages. All inter-service calls bypass Keycloak JWT authentication and use the shared XApiKey header for service identity.
Communication Model
All inter-service traffic routes through the Ocelot API Gateway. Services do not call each other's private CAE FQDNs directly. This ensures:
- A single choke point for circuit breaking and retry policies
- Uniform observability (all calls appear in Gateway logs)
- Consistent XApiKey validation
PublicApi NuGet Packages
Each service that exposes a public API consumed by other services provides a corresponding NuGet package:
| Package | Published by | Consumed by |
|---|---|---|
Microtec.PublicApi.AppsPortal.Accounting | Accounting service | HR, Sales, Inventory |
Microtec.PublicApi.AppsPortal.Inventory | Inventory service | Sales, Distribution |
Microtec.PublicApi.AppsPortal.HR | HR service | Payroll, Reporting |
Microtec.PublicApi.Notification | Notification service | All services |
Microtec.PublicApi.Attachment | Attachment service | All services |
Microtec.PublicApi.Workflow | Workflow service | Accounting, HR |
Packages are published to the Microtec Azure DevOps NuGet feed and versioned with the service release.
XApiKey Header
The XApiKey header identifies the caller as a trusted internal service:
XApiKey: 3bb564df-0f24-4ea6-82c1-d99f368cac8a| Property | Value |
|---|---|
| Header name | XApiKey |
| Value source | Key Vault secret XApiKey (per environment) |
| Gateway validation | Validates on routes tagged internal in Ocelot config |
| Client presentation | Added automatically by all Microtec.PublicApi.* clients |
XApiKey Differs Per Environment
The XApiKey value is environment-specific (different in dev, stage, preprod, uat, production). Services read it from their configuration at startup. When calling between environments (e.g. a local dev service calling stage), ensure the correct key is configured.
Typed HTTP Client Registration
Services register PublicApi clients using the shared extension:
csharp
// In Accounting service Program.cs
builder.Services.AddMicrotecPublicApiClients(builder.Configuration);
// Internally in Microtec.Web.Core
public static IServiceCollection AddMicrotecPublicApiClients(
this IServiceCollection services,
IConfiguration configuration)
{
var gatewayBaseUrl = configuration["Gateway:BaseUrl"]!;
var xApiKey = configuration["XApiKey"]!;
services.AddHttpClient<IInventoryPublicApi, InventoryPublicApiClient>(client =>
{
client.BaseAddress = new Uri(gatewayBaseUrl);
client.DefaultRequestHeaders.Add("XApiKey", xApiKey);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("inventory-resilience", ConfigureResilience);
return services;
}Polly Resilience Policies
All inter-service HTTP clients are configured with Polly resilience policies via Microsoft.Extensions.Http.Resilience:
csharp
private static void ConfigureResilience(ResiliencePipelineBuilder<HttpResponseMessage> builder)
{
builder
// Retry: 3 attempts with exponential backoff (1s, 2s, 4s)
.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode is
HttpStatusCode.RequestTimeout or
HttpStatusCode.TooManyRequests or
HttpStatusCode.BadGateway or
HttpStatusCode.ServiceUnavailable or
HttpStatusCode.GatewayTimeout)
})
// Circuit breaker: open after 5 failures in 30s, half-open after 60s
.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(60)
})
// Timeout per attempt
.AddTimeout(TimeSpan.FromSeconds(10));
}Resilience Policy Summary
| Policy | Config | Behaviour |
|---|---|---|
| Retry | 3 attempts, exponential backoff | Retries on network errors and 5xx/429 |
| Circuit breaker | 50% failure rate in 30s, 5 min break | Opens circuit after repeated failures |
| Timeout | 10s per attempt | Fails fast instead of blocking indefinitely |
Tenant Context Propagation
When one service calls another on behalf of a user, the tenant context must be propagated:
csharp
// Accounting service calling Inventory service
public class GetItemDetailsHandler
{
private readonly IInventoryPublicApi _inventoryApi;
private readonly ITenantProvider _tenantProvider;
public async Task<ItemDetails> Handle(GetItemDetailsQuery query, CancellationToken ct)
{
// TenantId is automatically added as a header by the client delegating handler
return await _inventoryApi.GetItemById(query.ItemId, ct);
}
}A TenantContextDelegatingHandler in the Microtec.PublicApi.* client adds the X-Tenant-Id header automatically:
csharp
public class TenantContextDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Add("X-Tenant-Id", _tenantProvider.TenantId);
return await base.SendAsync(request, ct);
}
}The receiving service reads X-Tenant-Id from the request headers and uses it to scope the database query — it does not trust the caller's tenant claim blindly. The XApiKey header is the trust signal; X-Tenant-Id is the data signal.
Error Handling
Inter-service call errors are handled by the calling service:
csharp
try
{
var item = await _inventoryApi.GetItemById(query.ItemId, ct);
return item;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
throw new NotFoundException($"Inventory item {query.ItemId} not found");
}
catch (BrokenCircuitException)
{
throw new ServiceUnavailableException("Inventory service is temporarily unavailable");
}Do Not Propagate Internal Errors to Clients
When an inter-service call fails, the calling service returns a user-friendly error (e.g. 503 Service Unavailable with a generic message). Never propagate internal stack traces or service-specific error codes to the ERP API response — this leaks implementation details.