C# / .NET
Complete integration guide for ImmutableLog in C# / .NET. Automatic middleware for ASP.NET Core and Minimal API — or a typed HttpClient client for workers, BackgroundServices, and jobs. Sending is async and does not block the response.
Middlewares
Automatic integration via middleware in the ASP.NET Core pipeline — every HTTP request is captured without modifying any controller or endpoint. Click the framework to see the complete code.
Middleware via IMiddleware with InvokeAsync(HttpContext). Captures Response.StatusCode after next and sends the event asynchronously.
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var sw = Stopwatch.StartNew();
await next(ctx);
_ = EmitAsync(ctx, ctx.Response.StatusCode, sw.Elapsed);
}View full documentation →
Same middleware registered with app.UseMiddleware<>(). Set event and trail per endpoint via HttpContext.Items.
app.UseMiddleware<ImmutableLogMiddleware>();
app.MapPost("/payments", (HttpContext ctx) =>
{
ctx.Items["imtbl.eventName"] = "payment.created";
return Results.Ok();
});View full documentation →
Typed HttpClient client
Use ImmutableLogClient to send events directly to ImmutableLog without depending on the web pipeline. Ideal for workers, BackgroundService, queue consumers, and any .NET code that needs to record events.
// ImmutableLogClient.cs
using System.Net.Http.Json;
using System.Text.Json;
public sealed class ImmutableLogClient
{
private readonly HttpClient _http;
private readonly string _apiKey;
private readonly string _service;
private readonly string _env;
public ImmutableLogClient(HttpClient http, string apiKey, string service, string env)
{
_http = http;
_apiKey = apiKey;
_service = service;
_env = env;
}
public async Task SendEventAsync(
string eventName,
string kind, // "success" | "info" | "error"
object payload,
string? immutableTrail = null,
CancellationToken ct = default)
{
var requestId = Guid.NewGuid().ToString();
var trail = SanitizeTrail(immutableTrail);
// Todos os valores de meta precisam ser strings.
var meta = new Dictionary<string, string>
{
["type"] = kind,
["event_name"] = eventName,
["service"] = _service,
["request_id"] = requestId,
["env"] = _env,
};
if (trail is not null) meta["immutable_trail"] = trail;
var body = new
{
payload = JsonSerializer.Serialize(payload),
meta,
};
using var req = new HttpRequestMessage(HttpMethod.Post, "/v1/events")
{
Content = JsonContent.Create(body),
};
req.Headers.Add("Authorization", $"Bearer {_apiKey}");
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
req.Headers.Add("Idempotency-Key", $"{eventName}-{requestId}");
req.Headers.Add("Request-Id", requestId);
// Timezone do cliente — usado pelo dashboard para exibir os timestamps.
req.Headers.Add("X-Client-TZ", "America/Sao_Paulo");
req.Headers.Add("X-Client-Offset-Minutes", "-180");
if (trail is not null) req.Headers.Add("X-Imtbl-Trail", trail);
using var res = await _http.SendAsync(req, ct);
res.EnsureSuccessStatusCode();
}
// Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
// O servidor responde 400 invalid_immutable_trail se a regra for violada.
public static string? SanitizeTrail(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var v = value.Trim().Replace(":", "-");
if (v.Length > 256) v = v[..256];
return v;
}
}
// Program.cs — registro via IHttpClientFactory (typed client)
builder.Services.AddHttpClient("immutablelog", c =>
{
c.BaseAddress = new Uri(
Environment.GetEnvironmentVariable("IMTBL_URL") ?? "https://api.immutablelog.com");
c.Timeout = TimeSpan.FromSeconds(5);
});
builder.Services.AddSingleton(sp =>
{
var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("immutablelog");
return new ImmutableLogClient(http,
Environment.GetEnvironmentVariable("IMTBL_API_KEY") ?? "",
"my-service", "production");
});
// Uso (fire-and-forget — descarta a Task; capture exceções se precisar):
// _ = client.SendEventAsync("user.created", "success",
// new { user_id = "usr_123", plan = "pro" }, immutableTrail: "order-7782");Use IHttpClientFactory to reuse connections. For fire-and-forget, discard the Task with _ = ...; wrap in try/catch if you want to handle send failures.
Retry with exponential backoff
For high-availability production, add retry with exponential backoff. ImmutableLog returns 202 on success and 200 on an idempotent duplicate — never retry on 429 (monthly limit reached).
// Retry em erros transitorios (nunca em 429).
public async Task SendWithRetryAsync(
string eventName, string kind, object payload,
string? trail = null, int maxAttempts = 3, CancellationToken ct = default)
{
var delay = TimeSpan.FromMilliseconds(500);
for (var attempt = 0; attempt < maxAttempts; attempt++)
{
try
{
await SendEventAsync(eventName, kind, payload, trail, ct);
return;
}
catch (HttpRequestException ex) when (ex.StatusCode == (System.Net.HttpStatusCode)429)
{
// 429 = limite mensal — nao fazer retry.
return;
}
catch (HttpRequestException) when (attempt + 1 < maxAttempts)
{
await Task.Delay(delay, ct);
delay *= 2; // 500ms -> 1s -> 2s
}
}
}Sensitive data hashing
Never send raw personal data (email, CPF, IP) to ImmutableLog. Use SHA-256 to generate a deterministic digest — traceable without exposing the original data.
using System.Security.Cryptography;
using System.Text;
// Faz o hash de dados sensiveis antes de enviar — nunca logue PII bruto.
static string Sha256Hex(string data)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
// Uso no payload do evento:
var payload = new
{
user_id = "usr_123",
email_hash = Sha256Hex("user@example.com"),
ip_hash = Sha256Hex("192.168.1.1"),
};This documentation reflects the current API behavior. For questions or advanced integrations, contact the support team.
