using Microsoft.EntityFrameworkCore; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Projection.ReadModel; var builder = WebApplication.CreateBuilder(args); // OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument incoming ASP.NET Core requests, exported // over OTLP to Tempo, so a BFF → projection-api read is one connected trace. Service name + OTLP // endpoint come from OTEL_* env (compose); the exporter no-ops when Tempo is unreachable. builder.Services.AddOpenTelemetry() .ConfigureResource(r => r.AddService( builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) .WithTracing(tracing => tracing .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") .AddHttpClientInstrumentation() .AddOtlpExporter()); var connectionString = builder.Configuration.GetConnectionString("Projection") ?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Projection'"); builder.Services.AddProjectionReadModel(connectionString); var app = builder.Build(); // Ensure the schema exists before serving reads. The Event Subscriber migrates this shared DB too; // MigrateProjectionAsync serialises concurrent migrators with a session advisory lock so the whole // migration sequence runs exactly once (see ServiceCollectionExtensions). await app.Services.MigrateProjectionAsync(); app.MapGet("/health", () => "Healthy"); // The read side of the projection. Public-safe field filtering is tightened in S-09; for now // the minimal projection only carries id + status (bsn/naam deferred — ADR-0008). app.MapGet("/register", async (ProjectionDbContext db, CancellationToken ct) => { var rows = await db.RegisterEntries .OrderBy(r => r.Id) .Select(r => new RegisterEntryDto(r.Id, r.Status, r.Reference, r.Bsn, r.NaamPlaceholder)) .ToListAsync(ct); return Results.Ok(rows); }); app.MapGet("/register/{id}", async (string id, ProjectionDbContext db, CancellationToken ct) => { var row = await db.RegisterEntries .Where(r => r.Id == id) .Select(r => new RegisterEntryDto(r.Id, r.Status, r.Reference, r.Bsn, r.NaamPlaceholder)) .SingleOrDefaultAsync(ct); return row is null ? Results.NotFound() : Results.Ok(row); }); await app.RunAsync(); public sealed record RegisterEntryDto(string Id, string Status, string? Reference, string? Bsn, string? NaamPlaceholder); public partial class Program;