C# / .NET Data Provider
This guide shows how to build a GW data provider using ASP.NET Core minimal APIs. The implementation uses dependency injection for services and inline middleware for API-key validation.
Project Setup
dotnet new web -n DataProvider
cd DataProvider
dotnet add package AWSSDK.S3
DataProvider.csproj (key packages)
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.*" />
</ItemGroup>
Models
Query Envelope
Models/QueryEnvelope.cs
using System.Text.Json.Serialization;
namespace DataProvider.Models;
public record QueryEnvelope
{
[JsonPropertyName("queryId")]
public string QueryId { get; init; } = "";
[JsonPropertyName("datasetId")]
public string? DatasetId { get; init; }
[JsonPropertyName("endpoint")]
public string? Endpoint { get; init; }
[JsonPropertyName("parameters")]
public Dictionary<string, object>? Parameters { get; init; }
[JsonPropertyName("delivery")]
public DeliveryConfig? Delivery { get; init; }
[JsonPropertyName("callbackUrl")]
public string CallbackUrl { get; init; } = "";
[JsonPropertyName("callbackToken")]
public string CallbackToken { get; init; } = "";
}
public record DeliveryConfig
{
[JsonPropertyName("mechanism")]
public string Mechanism { get; init; } = "inline";
[JsonPropertyName("url")]
public string? Url { get; init; }
[JsonPropertyName("s3Bucket")]
public string? S3Bucket { get; init; }
[JsonPropertyName("s3KeyPrefix")]
public string? S3KeyPrefix { get; init; }
}
Callback Payload
Models/CallbackPayload.cs
using System.Text.Json.Serialization;
namespace DataProvider.Models;
public class CallbackPayload
{
[JsonPropertyName("queryId")]
public string QueryId { get; set; } = "";
[JsonPropertyName("status")]
public string Status { get; set; } = "";
[JsonPropertyName("recordCount")]
public int RecordCount { get; set; }
[JsonPropertyName("executionTimeMs")]
public long ExecutionTimeMs { get; set; }
[JsonPropertyName("error")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Error { get; set; }
}
Delivery Service
Route results to S3, a webhook, or inline:
Services/DeliveryService.cs
using System.Text.Json;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using DataProvider.Models;
namespace DataProvider.Services;
public class DeliveryService
{
public record DeliveryResult(string Mechanism, string? Reference = null, int? StatusCode = null);
private readonly string _s3Endpoint;
private readonly string _s3Bucket;
private readonly HttpClient _http;
private readonly ILogger<DeliveryService> _logger;
public DeliveryService(HttpClient http, ILogger<DeliveryService> logger)
{
_http = http;
_logger = logger;
_s3Endpoint = Environment.GetEnvironmentVariable("S3_ENDPOINT") ?? "http://localstack:4566";
_s3Bucket = Environment.GetEnvironmentVariable("S3_BUCKET") ?? "gw-deliveries";
}
public async Task<DeliveryResult> DeliverResultsAsync(QueryEnvelope envelope, object results)
{
var mechanism = envelope.Delivery?.Mechanism ?? "inline";
return mechanism switch
{
"gw_s3_sync" => await DeliverToS3Async(envelope, results),
"webhook" => await DeliverToWebhookAsync(envelope, results),
_ => new DeliveryResult("inline"),
};
}
private async Task<DeliveryResult> DeliverToS3Async(QueryEnvelope envelope, object results)
{
var bucket = envelope.Delivery?.S3Bucket ?? _s3Bucket;
var prefix = envelope.Delivery?.S3KeyPrefix ?? "";
var key = $"{prefix}{envelope.QueryId}.json";
var s3Config = new AmazonS3Config
{
ServiceURL = _s3Endpoint,
ForcePathStyle = true
};
var credentials = new BasicAWSCredentials("test", "test");
using var s3Client = new AmazonS3Client(credentials, s3Config);
var json = JsonSerializer.Serialize(results);
await s3Client.PutObjectAsync(new PutObjectRequest
{
BucketName = bucket,
Key = key,
ContentBody = json,
ContentType = "application/json"
});
_logger.LogInformation("[delivery] S3 upload: s3://{Bucket}/{Key}", bucket, key);
return new DeliveryResult("gw_s3_sync", $"s3://{bucket}/{key}");
}
private async Task<DeliveryResult> DeliverToWebhookAsync(QueryEnvelope envelope, object results)
{
var url = envelope.Delivery?.Url
?? throw new InvalidOperationException("Webhook delivery requires delivery.url");
var json = JsonSerializer.Serialize(results);
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _http.PostAsync(url, content);
_logger.LogInformation("[delivery] Webhook POST to {Url} returned {StatusCode}",
url, (int)response.StatusCode);
return new DeliveryResult("webhook", null, (int)response.StatusCode);
}
}
Callback Service
HMAC-SHA256 signing and callback posting:
Services/CallbackService.cs
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using DataProvider.Models;
namespace DataProvider.Services;
public class CallbackService
{
private readonly HttpClient _http;
private readonly ILogger<CallbackService> _logger;
public CallbackService(HttpClient http, ILogger<CallbackService> logger)
{
_http = http;
_logger = logger;
}
public async Task PostCallbackAsync(
string callbackUrl, string callbackToken, CallbackPayload payload)
{
try
{
var body = JsonSerializer.SerializeToUtf8Bytes(payload);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(callbackToken));
var signatureBytes = hmac.ComputeHash(body);
var signature = Convert.ToHexStringLower(signatureBytes);
var request = new HttpRequestMessage(HttpMethod.Post, callbackUrl)
{
Content = new ByteArrayContent(body)
};
request.Content.Headers.ContentType = new("application/json");
request.Headers.Add("X-GW-Signature", $"sha256={signature}");
await _http.SendAsync(request);
}
catch (Exception ex)
{
// Log but don't throw -- GW will retry or poll
_logger.LogError("[callback] Failed for query {QueryId}: {Error}",
payload.QueryId, ex.Message);
}
}
}
Application Entry Point
Wire everything together using minimal APIs:
Program.cs
using System.Diagnostics;
using DataProvider.Models;
using DataProvider.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<CallbackService>();
builder.Services.AddHttpClient<DeliveryService>();
builder.Services.AddSingleton<MockDataService>();
var app = builder.Build();
var gwApiKey = Environment.GetEnvironmentVariable("GW_API_KEY") ?? "demo-api-key";
// API-key validation middleware
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/health"))
{
await next();
return;
}
var apiKey = context.Request.Headers["X-GW-Api-Key"].FirstOrDefault();
if (apiKey != gwApiKey)
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(
new { error = "Invalid or missing X-GW-Api-Key header" });
return;
}
await next();
});
// Health check
app.MapGet("/health", () =>
Results.Ok(new { status = "ok", service = "data-provider-csharp" }));
// Company search -- middleware pattern
app.MapPost("/api/search/companies", async (
QueryEnvelope envelope,
MockDataService mockData,
CallbackService callback,
DeliveryService delivery) =>
{
var sw = Stopwatch.StartNew();
if (string.IsNullOrEmpty(envelope.QueryId) || string.IsNullOrEmpty(envelope.CallbackUrl))
return Results.BadRequest(new { error = "Missing required envelope fields" });
try
{
var name = envelope.Parameters?.GetValueOrDefault("name")?.ToString();
var country = envelope.Parameters?.GetValueOrDefault("country")?.ToString();
var results = mockData.SearchCompanies(name, country);
var executionTimeMs = sw.ElapsedMilliseconds;
var deliveryResult = await delivery.DeliverResultsAsync(envelope, results);
await callback.PostCallbackAsync(
envelope.CallbackUrl, envelope.CallbackToken,
new CallbackPayload
{
QueryId = envelope.QueryId,
Status = "delivered",
RecordCount = results.Count,
ExecutionTimeMs = executionTimeMs
});
if (deliveryResult.Mechanism == "inline")
return Results.Ok(new {
queryId = envelope.QueryId, recordCount = results.Count,
executionTimeMs, results });
return Results.Ok(new {
queryId = envelope.QueryId, recordCount = results.Count,
executionTimeMs,
delivery = new { mechanism = deliveryResult.Mechanism,
reference = deliveryResult.Reference } });
}
catch (Exception ex)
{
await callback.PostCallbackAsync(
envelope.CallbackUrl, envelope.CallbackToken,
new CallbackPayload
{
QueryId = envelope.QueryId,
Status = "failed",
ExecutionTimeMs = sw.ElapsedMilliseconds,
Error = ex.Message
});
return Results.Json(
new { error = ex.Message, queryId = envelope.QueryId },
statusCode: 500);
}
});
// Sanctions check -- manual pattern
app.MapPost("/api/search/sanctions", async (
QueryEnvelope envelope,
MockDataService mockData,
CallbackService callback,
DeliveryService delivery) =>
{
var sw = Stopwatch.StartNew();
// Step 1: Validate envelope
if (string.IsNullOrEmpty(envelope.QueryId) || string.IsNullOrEmpty(envelope.CallbackUrl))
return Results.BadRequest(new { error = "Missing required envelope fields" });
// Step 2: Validate parameters
var name = envelope.Parameters?.GetValueOrDefault("name")?.ToString();
if (string.IsNullOrEmpty(name))
return Results.BadRequest(new {
error = "parameters.name is required for sanctions checks",
queryId = envelope.QueryId });
var country = envelope.Parameters?.GetValueOrDefault("country")?.ToString();
// Step 3: Run business logic
var results = mockData.CheckSanctions(name, country);
var executionTimeMs = sw.ElapsedMilliseconds;
try
{
// Step 4: Deliver results
var deliveryResult = await delivery.DeliverResultsAsync(envelope, results);
// Step 5: Post callback
await callback.PostCallbackAsync(
envelope.CallbackUrl, envelope.CallbackToken,
new CallbackPayload
{
QueryId = envelope.QueryId,
Status = "delivered",
RecordCount = results.Count,
ExecutionTimeMs = executionTimeMs
});
if (deliveryResult.Mechanism == "inline")
return Results.Ok(new {
queryId = envelope.QueryId, recordCount = results.Count,
executionTimeMs, results });
return Results.Ok(new {
queryId = envelope.QueryId, recordCount = results.Count,
executionTimeMs,
delivery = new { mechanism = deliveryResult.Mechanism,
reference = deliveryResult.Reference } });
}
catch (Exception ex)
{
await callback.PostCallbackAsync(
envelope.CallbackUrl, envelope.CallbackToken,
new CallbackPayload
{
QueryId = envelope.QueryId,
Status = "failed",
ExecutionTimeMs = sw.ElapsedMilliseconds,
Error = ex.Message
});
return Results.Json(
new { error = ex.Message, queryId = envelope.QueryId },
statusCode: 500);
}
});
app.Run();
Running the Provider
dotnet run
Or with environment variables:
GW_API_KEY=my-key S3_ENDPOINT=http://localhost:4566 dotnet run
Testing Locally
curl -X POST http://localhost:5000/api/search/sanctions \
-H "Content-Type: application/json" \
-H "X-GW-Api-Key: demo-api-key" \
-d '{
"queryId": "test-001",
"datasetId": "ds-sanctions",
"endpoint": "/api/search/sanctions",
"parameters": { "name": "Smith" },
"delivery": { "mechanism": "inline" },
"callbackUrl": "http://localhost:8083/webhook",
"callbackToken": "test-secret"
}'