517 lines
21 KiB
C#
517 lines
21 KiB
C#
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<PoweredSoft.Data.Core.IAsyncQueryableService, SimpleAsyncQueryableService>();
|
|
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
|
|
builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();
|
|
|
|
// 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<bool>("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<bool>("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<UserWorkflow>(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<InvitationWorkflow>(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<UserWorkflow>("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<InvitationWorkflow>("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<UserWorkflow>("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<UserAddedEvent>("Event emitted when a user is added to the system");
|
|
builder.Services.AddEvent<UserRemovedEvent>("Event emitted when a user is removed from the system");
|
|
builder.Services.AddEvent<UserInvitedEvent>("Event emitted when a user is invited");
|
|
builder.Services.AddEvent<UserInviteAcceptedEvent>("Event emitted when a user accepts an invitation");
|
|
builder.Services.AddEvent<UserInviteDeclinedEvent>("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<AddUserCommand, int, UserWorkflow, AddUserCommandHandler, AddUserCommandValidator>();
|
|
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
|
|
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
|
|
|
|
// 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<InviteUserCommand, string, InvitationWorkflow, InviteUserCommandHandler, InviteUserCommandValidator>();
|
|
// // builder.Services.AddCommandWithWorkflow<AcceptInviteCommand, int, InvitationWorkflow, AcceptInviteCommandHandler, AcceptInviteCommandValidator>();
|
|
// // builder.Services.AddCommandWithWorkflow<DeclineInviteCommand, InvitationWorkflow, DeclineInviteCommandHandler>();
|
|
|
|
// Register event consumer background service (demonstrates Phase 1.4 event consumption)
|
|
builder.Services.AddHostedService<EventConsumerBackgroundService>();
|
|
|
|
// Register RabbitMQ event consumer (demonstrates Phase 4 cross-service communication)
|
|
if (rabbitMqEnabled)
|
|
{
|
|
builder.Services.AddHostedService<RabbitMQEventConsumerBackgroundService>();
|
|
}
|
|
|
|
// ========================================================================
|
|
// 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<UserStatistics>();
|
|
|
|
// Register UserStatisticsProjection to build read model from UserWorkflow events
|
|
builder.Services.AddDynamicProjection<UserStatisticsProjection>(
|
|
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<OrderFulfillmentSaga>(
|
|
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<Svrnty.Sample.Invitations.SendInvitationCommand, Svrnty.Sample.Invitations.SendInvitationResult, Svrnty.Sample.Invitations.SendInvitationCommandHandler>();
|
|
// builder.Services.AddCommand<Svrnty.Sample.Invitations.AcceptInvitationCommand, Svrnty.Sample.Invitations.AcceptInvitationCommandHandler>();
|
|
// builder.Services.AddCommand<Svrnty.Sample.Invitations.DeclineInvitationCommand, Svrnty.Sample.Invitations.DeclineInvitationCommandHandler>();
|
|
// builder.Services.AddCommand<Svrnty.Sample.Invitations.SendInvitationReminderCommand, Svrnty.Sample.Invitations.SendInvitationReminderCommandHandler>();
|
|
|
|
// 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<ISchemaRegistry>();
|
|
await schemaRegistry.RegisterSchemaAsync<UserCreatedEventV1>(1); // Version 1
|
|
await schemaRegistry.RegisterSchemaAsync<UserCreatedEventV2>(2, typeof(UserCreatedEventV1)); // Version 2 (upcasts from V1)
|
|
await schemaRegistry.RegisterSchemaAsync<UserCreatedEventV3>(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<EventServiceImpl>();
|
|
|
|
// ========================================================================
|
|
// 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<OrderFulfillmentSaga>(
|
|
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
|
|
// ========================================================================
|
|
|
|
/// <summary>
|
|
/// Request model for starting an order fulfillment saga.
|
|
/// </summary>
|
|
public sealed record StartOrderRequest
|
|
{
|
|
public required string OrderId { get; init; }
|
|
public required List<Svrnty.Sample.Sagas.OrderItem> Items { get; init; }
|
|
public decimal Amount { get; init; }
|
|
public required string ShippingAddress { get; init; }
|
|
public bool SimulatePaymentFailure { get; init; } = false;
|
|
}
|