using Htmx.ApiDemo; using Htmx.ApiDemo.Data; using Immediate.Handlers; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver; // ── Explicit serializer registrations — force AOT to preserve constructors ─ BsonSerializer.RegisterSerializer(new ObjectIdSerializer()); BsonSerializer.RegisterSerializer(new StringSerializer()); BsonSerializer.RegisterSerializer(new DateTimeSerializer()); BsonSerializer.RegisterSerializer(new BooleanSerializer()); BsonSerializer.RegisterSerializer(new NullableSerializer(new DateTimeSerializer())); // ── Explicit BsonClassMap — no AutoMap() reflection, fully AOT-safe ─────── BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); cm.MapIdProperty(u => u.Id).SetSerializer(new ObjectIdSerializer()); cm.MapProperty(u => u.Email).SetElementName("email"); cm.MapProperty(u => u.NormalizedEmail).SetElementName("normalizedEmail"); cm.MapProperty(u => u.PasswordHash).SetElementName("passwordHash"); cm.MapProperty(u => u.DisplayName).SetElementName("displayName"); cm.MapProperty(u => u.CreatedAtUtc).SetElementName("createdAtUtc"); cm.SetIgnoreExtraElements(true); }); var builder = WebApplication.CreateSlimBuilder(args); // ── Antiforgery ─────────────────────────────────────────────────────────── builder.Services.AddAntiforgery(); // ── JSON ────────────────────────────────────────────────────────────────── builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); }); // ── MongoDB ─────────────────────────────────────────────────────────────── builder.Services.AddSingleton( new MongoClient(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); // ── Cookie Authentication ───────────────────────────────────────────────── builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.LogoutPath = "/logout"; options.AccessDeniedPath = "/login"; options.SlidingExpiration = true; options.ExpireTimeSpan = TimeSpan.FromHours(8); }); builder.Services.AddScoped, PasswordHasher>(); builder.Services.AddScoped(); // ── App services ────────────────────────────────────────────────────────── builder.Services.AddHttpContextAccessor(); builder.Services .AddHtmxApiDemoBehaviors() .AddHtmxApiDemoHandlers(); builder.Services.AddOpenApi(); builder.Services.AddAuthorization(); var app = builder.Build(); // Ensure the unique index on NormalizedEmail exists (runs once on startup, idempotent). using (var scope = app.Services.CreateScope()) await scope.ServiceProvider.GetRequiredService().EnsureIndexesAsync(); if (app.Environment.IsDevelopment()) app.MapOpenApi(); app.UseStaticFiles(); app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); // ── Guard: redirect unauthenticated users to /login ─────────────────────── app.Use(async (context, next) => { var path = context.Request.Path.Value ?? ""; bool isPublic = path.StartsWith("/login", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/register", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/logout", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/css/", StringComparison.OrdinalIgnoreCase) || path.StartsWith("/js/", StringComparison.OrdinalIgnoreCase); if (!isPublic && context.User.Identity?.IsAuthenticated != true) { context.Response.Redirect("/login"); return; } await next(); }); // Explicit MapGet/MapPost with explicit lambda return type ValueTask. // The explicit return type annotation lets RequestDelegateGenerator see IResult // directly (without needing to resolve the generated Handler.HandleAsync type), // so it emits `await result.ExecuteAsync(httpContext)` instead of JSON serialization. app.MapGet("/", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.GetIndexHandler.Command cmd, Htmx.ApiDemo.Templates.GetIndexHandler.Handler handler, CancellationToken token) => handler.HandleAsync(cmd, token)); app.MapGet("/greet/{username}/{count?}/{id?}", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.GetGreetingHandler.Query query, Htmx.ApiDemo.Templates.GetGreetingHandler.Handler handler, CancellationToken token) => handler.HandleAsync(query, token)); app.MapGet("/login", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.GetLoginHandler.Query query, Htmx.ApiDemo.Templates.GetLoginHandler.Handler handler, CancellationToken token) => handler.HandleAsync(query, token)); app.MapPost("/login", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.PostLoginHandler.Command cmd, Htmx.ApiDemo.Templates.PostLoginHandler.Handler handler, CancellationToken token) => handler.HandleAsync(cmd, token)); app.MapGet("/register", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.GetRegisterHandler.Query query, Htmx.ApiDemo.Templates.GetRegisterHandler.Handler handler, CancellationToken token) => handler.HandleAsync(query, token)); app.MapPost("/register", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.PostRegisterHandler.Command cmd, Htmx.ApiDemo.Templates.PostRegisterHandler.Handler handler, CancellationToken token) => handler.HandleAsync(cmd, token)); app.MapPost("/logout", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.PostLogoutHandler.Command cmd, Htmx.ApiDemo.Templates.PostLogoutHandler.Handler handler, CancellationToken token) => handler.HandleAsync(cmd, token)); app.MapGet("/ui-demo", static ValueTask ( [AsParameters] Htmx.ApiDemo.Templates.GetUiDemoHandler.Query query, Htmx.ApiDemo.Templates.GetUiDemoHandler.Handler handler, CancellationToken token) => handler.HandleAsync(query, token)); app.Run();