using Microsoft.AspNetCore.Server.Kestrel.Core; using Svrnty.CQRS.Events.Abstractions.Subscriptions; using Svrnty.Sample.Commands; using Svrnty.Sample.Workflows; using Svrnty.Sample.Events; using Svrnty.CQRS.Events.Abstractions.Schema; using Svrnty.CQRS.Events.Abstractions.Models; using Svrnty.CQRS; using Svrnty.CQRS.FluentValidation; using Svrnty.CQRS.Grpc; using Svrnty.Sample; using Svrnty.Sample.Projections; using Svrnty.Sample.Sagas; // using Svrnty.Sample.Invitations; // Phase 8 - temporarily disabled using Svrnty.CQRS.MinimalApi; using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.Events; using Svrnty.CQRS.Events.Abstractions; using Svrnty.CQRS.Events.Abstractions.Sagas; using Svrnty.CQRS.Events.Grpc; using Svrnty.CQRS.Events.PostgreSQL; using Svrnty.CQRS.Events.PostgreSQL.Subscriptions; using Svrnty.CQRS.Events.RabbitMQ; using Svrnty.CQRS.Events.Projections; using Svrnty.CQRS.Events.Sagas; using Svrnty.CQRS.Events.Subscriptions; using Svrnty.Sample.Queries; using Svrnty.Sample.Services; using Svrnty.CQRS.Events.Abstractions.Streaming; using Svrnty.CQRS.Events.Abstractions.Delivery; using Svrnty.Sample.BackgroundServices; // using Svrnty.CQRS.Events.SignalR; // Phase 8 - temporarily disabled var builder = WebApplication.CreateBuilder(args); // Configure Kestrel to support both HTTP/1.1 (for REST APIs) and HTTP/2 (for gRPC) builder.WebHost.ConfigureKestrel(options => { // Port 6000: HTTP/2 for gRPC options.ListenLocalhost(6000, o => o.Protocols = HttpProtocols.Http2); // Port 6001: HTTP/1.1 for HTTP API options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); }); // IMPORTANT: Register dynamic query dependencies FIRST // (before AddSvrntyCqrs, so gRPC services can find the handlers) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddDynamicQueryWithProvider(); // Add event streaming support builder.Services.AddSvrntyEvents(); builder.Services.AddDefaultEventDiscovery(); // Phase 5: Add schema evolution support for event versioning builder.Services.AddSchemaEvolution(); builder.Services.AddJsonSchemaGeneration(); // Configure event storage (PostgreSQL or in-memory) var usePostgreSQL = builder.Configuration.GetValue("EventStreaming:UsePostgreSQL"); if (usePostgreSQL) { builder.Services.AddPostgresEventStreaming( builder.Configuration.GetSection("EventStreaming:PostgreSQL")); builder.Services.AddPostgresSchemaStore(); // Use PostgreSQL for schema storage } else { builder.Services.AddInMemoryEventStorage(); // Use in-memory storage for demo } builder.Services.AddSvrntyEventsGrpc(); // Enable gRPC event streaming // Configure RabbitMQ for cross-service event streaming (Phase 4) var rabbitMqEnabled = builder.Configuration.GetValue("EventStreaming:RabbitMQ:Enabled"); if (rabbitMqEnabled) { builder.Services.AddRabbitMQEventDelivery(options => { builder.Configuration.GetSection("EventStreaming:RabbitMQ").Bind(options); }); } // Configure event streams and subscriptions (Phase 1.2+) builder.Services.AddEventStreaming(streaming => { // Configure stream for UserWorkflow // Phase 4: Changed to CrossService scope to publish events to RabbitMQ streaming.AddStream(stream => { stream.Type = StreamType.Ephemeral; stream.DeliverySemantics = DeliverySemantics.AtLeastOnce; stream.Scope = rabbitMqEnabled ? StreamScope.CrossService : StreamScope.Internal; }); // // Configure stream for InvitationWorkflow (Phase 8 - temporarily disabled) // // Phase 4: Changed to CrossService scope to publish events to RabbitMQ // streaming.AddStream(stream => // { // stream.Type = StreamType.Ephemeral; // stream.DeliverySemantics = DeliverySemantics.AtLeastOnce; // stream.Scope = rabbitMqEnabled ? StreamScope.CrossService : StreamScope.Internal; // }); // Add a broadcast subscription for analytics/logging // All consumers receive all events (great for logging, analytics, audit) streaming.AddSubscription("user-analytics", sub => { sub.Mode = SubscriptionMode.Broadcast; sub.VisibilityTimeout = TimeSpan.FromSeconds(30); }); // Add an exclusive subscription for processing (Phase 8 - temporarily disabled) // Only one consumer receives each event (great for work distribution) // streaming.AddSubscription("invitation-processor", sub => // { // sub.Mode = SubscriptionMode.Exclusive; // sub.VisibilityTimeout = TimeSpan.FromSeconds(30); // }); // Phase 5: Add a subscription with automatic upcasting enabled // Demonstrates schema evolution - old events are automatically upgraded to latest version streaming.AddSubscription("user-versioning-demo", sub => { sub.Mode = SubscriptionMode.Broadcast; sub.EnableUpcasting = true; // Automatically upcast events to latest version sub.TargetEventVersion = null; // null = upcast to latest version sub.Description = "Phase 5: Demonstrates automatic event upcasting for schema evolution"; }); }); // Register events for discovery builder.Services.AddEvent("Event emitted when a user is added to the system"); builder.Services.AddEvent("Event emitted when a user is removed from the system"); builder.Services.AddEvent("Event emitted when a user is invited"); builder.Services.AddEvent("Event emitted when a user accepts an invitation"); builder.Services.AddEvent("Event emitted when a user declines an invitation"); // Register commands and queries with validators // NEW: Using workflow-based registration (recommended for event-emitting commands) builder.Services.AddCommandWithWorkflow(); builder.Services.AddCommand(); builder.Services.AddQuery(); // Register multi-step workflow commands (invite user flow) // Phase 1: Each command creates its own workflow instance // Future phases will support workflow continuation for true multi-step correlation // // builder.Services.AddCommandWithWorkflow(); // // builder.Services.AddCommandWithWorkflow(); // // builder.Services.AddCommandWithWorkflow(); // Register event consumer background service (demonstrates Phase 1.4 event consumption) builder.Services.AddHostedService(); // Register RabbitMQ event consumer (demonstrates Phase 4 cross-service communication) if (rabbitMqEnabled) { builder.Services.AddHostedService(); } // ======================================================================== // Phase 7: Event Sourcing Projections // ======================================================================== // Register projection infrastructure builder.Services.AddProjections(useInMemoryCheckpoints: !usePostgreSQL); // If using PostgreSQL, register persistent checkpoint store if (usePostgreSQL) { builder.Services.AddPostgresProjectionCheckpointStore(); } // Register UserStatistics as singleton (shared across projection instances) builder.Services.AddSingleton(); // Register UserStatisticsProjection to build read model from UserWorkflow events builder.Services.AddDynamicProjection( projectionName: "user-statistics", streamName: "UserWorkflow", // Must match stream name registered above configure: options => { options.BatchSize = 50; // Process 50 events per batch options.AutoStart = true; // Auto-start on application startup options.MaxRetries = 3; // Retry failed events 3 times options.CheckpointPerEvent = false; // Checkpoint after each batch (not per event) options.AllowRebuild = true; // Allow projection rebuilds options.PollingInterval = TimeSpan.FromSeconds(1); // Poll for new events every second }); // ======================================================================== // Phase 7: Saga Orchestration // ======================================================================== // Register saga infrastructure builder.Services.AddSagaOrchestration(useInMemoryStateStore: !usePostgreSQL); // If using PostgreSQL, register persistent state store if (usePostgreSQL) { builder.Services.AddPostgresSagaStateStore(); } // Register OrderFulfillmentSaga with compensation steps builder.Services.AddSaga( sagaName: "order-fulfillment", configure: definition => { // Step 1: Reserve inventory (can be compensated by releasing reservation) definition.AddStep( stepName: "ReserveInventory", execute: OrderFulfillmentSteps.ReserveInventoryAsync, compensate: OrderFulfillmentSteps.CompensateReserveInventoryAsync); // Step 2: Authorize payment (can be compensated by voiding authorization) definition.AddStep( stepName: "AuthorizePayment", execute: OrderFulfillmentSteps.AuthorizePaymentAsync, compensate: OrderFulfillmentSteps.CompensateAuthorizePaymentAsync); // Step 3: Ship order (can be compensated by cancelling shipment) definition.AddStep( stepName: "ShipOrder", execute: OrderFulfillmentSteps.ShipOrderAsync, compensate: OrderFulfillmentSteps.CompensateShipOrderAsync); }); // ======================================================================== // Phase 8: Persistent Subscriptions & Bidirectional Communication // ======================================================================== // Add SignalR support // builder.Services.AddSignalR(); // Add persistent subscription infrastructure builder.Services.AddPersistentSubscriptions( useInMemoryStore: !usePostgreSQL, enableBackgroundDelivery: true); // If using PostgreSQL, register persistent subscription store if (usePostgreSQL) { builder.Services.AddPostgresSubscriptionStore(); } // Add SignalR hubs (Phase 8 - temporarily disabled) // builder.Services.AddPersistentSubscriptionHub(); // builder.Services.AddEventStreamHub(); // Register invitation command handlers // builder.Services.AddCommand(); // builder.Services.AddCommand(); // builder.Services.AddCommand(); // builder.Services.AddCommand(); // Configure CQRS with fluent API builder.Services.AddSvrntyCqrs(cqrs => { // Enable gRPC endpoints with reflection cqrs.AddGrpc(grpc => { grpc.EnableReflection(); }); // Enable MinimalApi endpoints cqrs.AddMinimalApi(configure => { }); }); builder.Services.AddEndpointsApiExplorer(); // builder.Services.AddSwaggerGen(); // Temporarily disabled due to version incompatibility var app = builder.Build(); // Phase 5: Register event schemas for versioning // This should be done once at application startup var schemaRegistry = app.Services.GetRequiredService(); await schemaRegistry.RegisterSchemaAsync(1); // Version 1 await schemaRegistry.RegisterSchemaAsync(2, typeof(UserCreatedEventV1)); // Version 2 (upcasts from V1) await schemaRegistry.RegisterSchemaAsync(3, typeof(UserCreatedEventV2)); // Version 3 (upcasts from V2) Console.WriteLine("✓ Registered 3 versions of UserCreatedEvent schema with automatic upcasting"); Console.WriteLine(); // Map all configured CQRS endpoints (gRPC, MinimalApi, and Dynamic Queries) // This automatically maps CommandServiceImpl, QueryServiceImpl, and DynamicQueryServiceImpl app.UseSvrntyCqrs(); // Map event streaming service (not part of auto-generated services) app.MapGrpcService(); // ======================================================================== // Phase 7: Projection Query Endpoints // ======================================================================== // Add HTTP endpoint to query user statistics projection app.MapGet("/api/projections/user-statistics", (UserStatistics stats) => { return Results.Ok(new { TotalAdded = stats.TotalUsersAdded, TotalRemoved = stats.TotalUsersRemoved, CurrentCount = stats.CurrentUserCount, LastUpdated = stats.LastUpdated, LastUser = new { Id = stats.LastUserId, Name = stats.LastUserName, Email = stats.LastUserEmail } }); }) .WithName("GetUserStatistics") .WithTags("Projections"); // ======================================================================== // Phase 7: Saga Orchestration Endpoints // ======================================================================== // Start a new order fulfillment saga app.MapPost("/api/sagas/order-fulfillment/start", async ( ISagaOrchestrator orchestrator, StartOrderRequest request) => { try { var initialData = new Svrnty.CQRS.Events.Sagas.SagaData(); initialData.Set("OrderId", request.OrderId); initialData.Set("Items", request.Items); initialData.Set("Amount", request.Amount); initialData.Set("ShippingAddress", request.ShippingAddress); initialData.Set("FailPayment", request.SimulatePaymentFailure); // For testing compensation var sagaId = await orchestrator.StartSagaAsync( correlationId: request.OrderId, initialData: initialData); Console.WriteLine($"✓ Started OrderFulfillmentSaga with ID: {sagaId}"); return Results.Ok(new { SagaId = sagaId, CorrelationId = request.OrderId, Message = request.SimulatePaymentFailure ? "Saga started (payment will fail to demonstrate compensation)" : "Saga started successfully" }); } catch (Exception ex) { return Results.Problem(ex.Message); } }) .WithName("StartOrderFulfillmentSaga") .WithTags("Sagas"); // Get saga status app.MapGet("/api/sagas/{sagaId}/status", async ( ISagaOrchestrator orchestrator, string sagaId) => { try { var status = await orchestrator.GetStatusAsync(sagaId); return Results.Ok(new { status.SagaId, status.CorrelationId, status.SagaName, State = status.State.ToString(), Progress = $"{status.CurrentStep}/{status.TotalSteps}", status.StartedAt, status.LastUpdated, status.CompletedAt, status.ErrorMessage, status.Data }); } catch (Exception ex) { return Results.Problem(ex.Message); } }) .WithName("GetSagaStatus") .WithTags("Sagas"); // Cancel a running saga app.MapPost("/api/sagas/{sagaId}/cancel", async ( ISagaOrchestrator orchestrator, string sagaId) => { try { await orchestrator.CancelSagaAsync(sagaId); return Results.Ok(new { SagaId = sagaId, Message = "Saga cancellation initiated (compensation in progress)" }); } catch (Exception ex) { return Results.Problem(ex.Message); } }) .WithName("CancelSaga") .WithTags("Sagas"); // ======================================================================== // Phase 8: Persistent Subscription Endpoints // ======================================================================== // Map SignalR hubs (Phase 8 - temporarily disabled) // app.MapPersistentSubscriptionHub("/hubs/subscriptions"); // app.MapEventStreamHub("/hubs/events"); // Map invitation endpoints // app.MapInvitationEndpoints(); // app.UseSwagger(); // Temporarily disabled // app.UseSwaggerUI(); Console.WriteLine("=== Svrnty CQRS Sample with Event Streaming ==="); Console.WriteLine(); Console.WriteLine("gRPC (HTTP/2): http://localhost:6000"); Console.WriteLine(" - CommandService, QueryService, DynamicQueryService"); Console.WriteLine(" - EventService (bidirectional streaming)"); Console.WriteLine(); Console.WriteLine("HTTP API (HTTP/1.1): http://localhost:6001"); Console.WriteLine(" - Commands: POST /api/command/*"); Console.WriteLine(" - Queries: GET/POST /api/query/*"); Console.WriteLine(" - Swagger UI: http://localhost:6001/swagger"); Console.WriteLine(); Console.WriteLine("Event Streams Configured:"); Console.WriteLine(" - UserWorkflow stream (ephemeral, at-least-once, {0})", rabbitMqEnabled ? "external via RabbitMQ" : "internal"); // Console.WriteLine(" - InvitationWorkflow stream (ephemeral, at-least-once, {0})", // rabbitMqEnabled ? "external via RabbitMQ" : "internal"); Console.WriteLine(); Console.WriteLine("Subscriptions Active:"); Console.WriteLine(" - user-analytics (broadcast mode, internal)"); Console.WriteLine(" - invitation-processor (exclusive mode, internal)"); Console.WriteLine(" - user-versioning-demo (broadcast mode, with auto-upcasting enabled)"); if (rabbitMqEnabled) { Console.WriteLine(" - email-service (consumer group mode, RabbitMQ)"); } Console.WriteLine(); Console.WriteLine("Schema Evolution (Phase 5):"); Console.WriteLine(" - UserCreatedEvent: 3 versions registered (V1 → V2 → V3)"); Console.WriteLine(" - Auto-upcasting: Enabled on user-versioning-demo subscription"); Console.WriteLine(" - JSON Schema: Auto-generated for external consumers"); Console.WriteLine(); Console.WriteLine("Event Sourcing Projections (Phase 7.1):"); Console.WriteLine(" - user-statistics: Tracks user statistics from UserWorkflow events"); Console.WriteLine(" - Query endpoint: GET /api/projections/user-statistics"); Console.WriteLine(" - Checkpoint storage: {0}", usePostgreSQL ? "PostgreSQL (persistent)" : "In-memory"); Console.WriteLine(); Console.WriteLine("Saga Orchestration (Phase 7.3):"); Console.WriteLine(" - order-fulfillment: Multi-step workflow with compensation"); Console.WriteLine(" - Start saga: POST /api/sagas/order-fulfillment/start"); Console.WriteLine(" - Get status: GET /api/sagas/{{sagaId}}/status"); Console.WriteLine(" - Cancel saga: POST /api/sagas/{{sagaId}}/cancel"); Console.WriteLine(" - State storage: {0}", usePostgreSQL ? "PostgreSQL (persistent)" : "In-memory"); Console.WriteLine(); Console.WriteLine("Persistent Subscriptions (Phase 8):"); Console.WriteLine(" - SignalR Hub: ws://localhost:6001/hubs/subscriptions"); Console.WriteLine(" - Event Stream Hub: ws://localhost:6001/hubs/events"); Console.WriteLine(" - Invitation workflow: Demonstrates correlation-based subscriptions"); Console.WriteLine(" - Send invitation: POST /api/invitations/send"); Console.WriteLine(" - Accept invitation: POST /api/invitations/{{id}}/accept"); Console.WriteLine(" - Decline invitation: POST /api/invitations/{{id}}/decline"); Console.WriteLine(" - Subscription storage: {0}", usePostgreSQL ? "PostgreSQL (persistent)" : "In-memory"); Console.WriteLine(" - Background delivery: Enabled"); Console.WriteLine(); if (rabbitMqEnabled) { Console.WriteLine("RabbitMQ Configuration:"); Console.WriteLine(" - Connection: amqp://localhost:5672"); Console.WriteLine(" - Exchange Prefix: svrnty-sample"); Console.WriteLine(" - Management UI: http://localhost:15672 (guest/guest)"); Console.WriteLine(); } Console.WriteLine("Infrastructure:"); Console.WriteLine(" - PostgreSQL: localhost:5432 (svrnty_events)"); if (rabbitMqEnabled) { Console.WriteLine(" - RabbitMQ: localhost:5672"); } Console.WriteLine(); Console.WriteLine("To start infrastructure:"); Console.WriteLine(" docker-compose up -d"); Console.WriteLine(); app.Run(); // ======================================================================== // Request Models // ======================================================================== /// /// Request model for starting an order fulfillment saga. /// public sealed record StartOrderRequest { public required string OrderId { get; init; } public required List Items { get; init; } public decimal Amount { get; init; } public required string ShippingAddress { get; init; } public bool SimulatePaymentFailure { get; init; } = false; }