using JSMR.Application.Circles.Queries.Search; using JSMR.Application.Creators.Queries.Search; using JSMR.Application.DI; using JSMR.Application.Tags.Queries.Search; using JSMR.Application.VoiceWorks.Queries.Search; using JSMR.Infrastructure.Data; using JSMR.Infrastructure.DI; using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; using Serilog; using Serilog.Events; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services .AddMemoryCache() // TODO .AddApplication() .AddInfrastructure(); // DbContext (MySQL here; swap to Npgsql when you migrate) var cs = builder.Configuration.GetConnectionString("AppDb") ?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb2"); builder.Services.AddDbContextFactory(opt => opt.UseMySql(cs, ServerVersion.AutoDetect(cs)) .EnableSensitiveDataLogging(false)); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); builder.Services.Configure(options => { options.SerializerOptions.PropertyNameCaseInsensitive = true; options.SerializerOptions.Converters.Add( new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); // or null for exact names }); // Serilog bootstrap (before Build) Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.WithProperty("Service", "JSMR.Api") .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName) .CreateLogger(); builder.Host.UseSerilog(); builder.Services.AddCors(o => { o.AddPolicy("ui-dev", p => p .AllowAnyOrigin() //.WithOrigins( // "*", // "https://localhost:5173", // vite-like // "https://localhost:5001", // typical https dev // "http://localhost:5000", // typical http dev // "https://localhost:7112", // blazor wasm dev https (adjust to your port) // "http://localhost:5153" // blazor wasm dev http (adjust to your port) //) .AllowAnyHeader() .AllowAnyMethod()); }); var app = builder.Build(); app.UseCors("ui-dev"); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); // Request logging with latency, status, path app.UseSerilogRequestLogging(opts => { opts.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; }); // Correlation: ensure every log has traceId and return it to clients app.Use(async (ctx, next) => { // Use current Activity if present (W3C trace context), else fall back var traceId = Activity.Current?.TraceId.ToString() ?? ctx.TraceIdentifier; using (Serilog.Context.LogContext.PushProperty("TraceId", traceId)) { ctx.Response.Headers["x-trace-id"] = traceId; await next(); } }); // Health check app.MapGet("/health", () => Results.Ok(new { status = "ok" })); // ---- Endpoints ---- // Circles Search app.MapPost("/api/circles/search", async ( SearchCirclesRequest request, SearchCirclesHandler handler, CancellationToken cancallationToken) => { try { SearchCirclesResponse result = await handler.HandleAsync(request, cancallationToken); return Results.Ok(result); } catch (OperationCanceledException) when (cancallationToken.IsCancellationRequested) { return Results.StatusCode(StatusCodes.Status499ClientClosedRequest); } }); // Voice Works Search app.MapPost("/api/voiceworks/search", async ( SearchVoiceWorksRequest request, SearchVoiceWorksHandler handler, CancellationToken cancallationToken) => { var result = await handler.HandleAsync(request, cancallationToken); return Results.Ok(result); }); // Tags Search app.MapPost("/api/tags/search", async ( SearchTagsRequest request, SearchTagsHandler handler, CancellationToken cancallationToken) => { var result = await handler.HandleAsync(request, cancallationToken); return Results.Ok(result); }); // Creators Search app.MapPost("/api/creators/search", async ( SearchCreatorsRequest request, SearchCreatorsHandler handler, CancellationToken cancallationToken) => { var result = await handler.HandleAsync(request, cancallationToken); return Results.Ok(result); }); app.Run();