# Getting Started > Step-by-step guide to building a CQRS application with Svrnty.CQRS on .NET 10. ## Prerequisites - .NET 10 SDK - A text editor or IDE with C# support ## 1. Create a New Project ```bash dotnet new web -n MyCqrsApp cd MyCqrsApp ``` Add the required packages: ```bash dotnet add package Svrnty.CQRS dotnet add package Svrnty.CQRS.Abstractions dotnet add package Svrnty.CQRS.MinimalApi dotnet add package Svrnty.CQRS.FluentValidation ``` ## 2. Define Commands and Queries ### Command with Result A command represents an action that changes state. Implement `ICommandHandler` for commands that return a value. ```csharp using Svrnty.CQRS.Abstractions; // The command (a plain record/class) public record CreateUserCommand { public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; public int Age { get; set; } } // The handler public class CreateUserCommandHandler : ICommandHandler { public Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default) { // Your business logic here -- persist to database, etc. return Task.FromResult(123); // Return the new user ID } } ``` ### Command without Result For commands that do not return a value, implement `ICommandHandler`: ```csharp public record DeleteUserCommand { public int UserId { get; set; } } public class DeleteUserCommandHandler : ICommandHandler { public Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken = default) { // Delete the user return Task.CompletedTask; } } ``` ### Query A query retrieves data without side effects. Implement `IQueryHandler`: ```csharp public record GetUserQuery { public int UserId { get; set; } } public record UserDto { public int Id { get; set; } public string Name { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; } public class GetUserQueryHandler : IQueryHandler { public Task HandleAsync(GetUserQuery query, CancellationToken cancellationToken = default) { return Task.FromResult(new UserDto { Id = query.UserId, Name = "John Doe", Email = "john@example.com" }); } } ``` ## 3. Register Handlers In `Program.cs`, register your handlers with the DI container: ```csharp using Svrnty.CQRS; using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.MinimalApi; var builder = WebApplication.CreateBuilder(args); // Register command and query handlers builder.Services.AddCommand(); builder.Services.AddCommand(); builder.Services.AddQuery(); // Configure CQRS with MinimalApi transport builder.Services.AddSvrntyCqrs(cqrs => { cqrs.AddMinimalApi(); }); var app = builder.Build(); // Map all CQRS endpoints app.UseSvrntyCqrs(); app.Run(); ``` This will expose: - `POST /api/command/CreateUser` -- executes CreateUserCommand - `POST /api/command/DeleteUser` -- executes DeleteUserCommand - `POST /api/query/GetUser` -- executes GetUserQuery ## 4. Add FluentValidation Add validators to enforce business rules before handler execution: ```csharp using FluentValidation; public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Email must be valid"); RuleFor(x => x.Name) .NotEmpty().WithMessage("Name is required"); RuleFor(x => x.Age) .GreaterThan(0).WithMessage("Age must be greater than 0"); } } ``` Register the command with its validator using the 4-type-parameter overload: ```csharp builder.Services.AddCommand(); ``` Validation errors are returned as RFC 7807 Problem Details (HTTP) or Google Rich Error Model (gRPC). ## 5. gRPC Setup Add the gRPC packages: ```bash dotnet add package Svrnty.CQRS.Grpc dotnet add package Svrnty.CQRS.Grpc.Generators dotnet add package Svrnty.CQRS.Grpc.Abstractions ``` Configure Kestrel for dual-protocol support and enable gRPC: ```csharp using Microsoft.AspNetCore.Server.Kestrel.Core; using Svrnty.CQRS.Grpc; var builder = WebApplication.CreateBuilder(args); // Configure dual ports builder.WebHost.ConfigureKestrel(options => { options.ListenLocalhost(6000, o => o.Protocols = HttpProtocols.Http2); // gRPC options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); // HTTP API }); // Register handlers (same as before) builder.Services.AddCommand(); builder.Services.AddQuery(); // Enable both gRPC and MinimalApi builder.Services.AddSvrntyCqrs(cqrs => { cqrs.AddGrpc(grpc => { grpc.EnableReflection(); // Enable gRPC reflection for tools like grpcurl }); cqrs.AddMinimalApi(); }); var app = builder.Build(); app.UseSvrntyCqrs(); app.Run(); ``` The `Svrnty.CQRS.Grpc.Generators` package automatically generates `.proto` files and gRPC service implementations from your registered command/query types at build time. ### Excluding Commands from gRPC Use the `[GrpcIgnore]` attribute to prevent a command or query from being exposed via gRPC: ```csharp using Svrnty.CQRS.Grpc.Abstractions.Attributes; [GrpcIgnore] public record InternalCommand { public string Data { get; set; } = string.Empty; } ``` ## 6. DynamicQuery Usage Dynamic queries provide automatic filtering, sorting, grouping, and pagination for entity collections. Add the packages: ```bash dotnet add package Svrnty.CQRS.DynamicQuery dotnet add package Svrnty.CQRS.DynamicQuery.Abstractions dotnet add package Svrnty.CQRS.DynamicQuery.MinimalApi ``` ### Define a Queryable Provider Implement `IQueryableProvider` to supply the data source: ```csharp using Svrnty.CQRS.DynamicQuery.Abstractions; public class UserQueryableProvider : IQueryableProvider { private readonly MyDbContext _db; public UserQueryableProvider(MyDbContext db) { _db = db; } public Task> GetQueryableAsync(object query, CancellationToken cancellationToken = default) { return Task.FromResult(_db.Users.AsQueryable()); } } ``` ### Register the Provider ```csharp using Svrnty.CQRS.DynamicQuery; // Register PoweredSoft dependencies builder.Services.AddTransient(); builder.Services.AddTransient(); // Register the dynamic query provider builder.Services.AddDynamicQueryWithProvider(); ``` This exposes a POST endpoint that accepts filter, sort, group, and pagination parameters, returning paged results automatically. ### Entity Framework Integration For EF Core projects, add the EF integration package: ```bash dotnet add package Svrnty.CQRS.DynamicQuery.EntityFramework ``` This provides a ready-made `IAsyncQueryableService` backed by EF Core. ## 7. Domain Events Domain events allow you to publish side effects after a command completes. Add the packages: ```bash dotnet add package Svrnty.CQRS.Events.Abstractions dotnet add package Svrnty.CQRS.Events.RabbitMQ # or implement your own IDomainEventPublisher ``` ### Define an Event ```csharp using Svrnty.CQRS.Events.Abstractions; public record UserCreatedEvent : IDomainEvent { public Guid EventId { get; } = Guid.NewGuid(); public DateTime OccurredAt { get; } = DateTime.UtcNow; public int UserId { get; init; } public string Email { get; init; } = string.Empty; } ``` ### Publish from a Command Handler ```csharp public class CreateUserCommandHandler : ICommandHandler { private readonly IDomainEventPublisher _events; public CreateUserCommandHandler(IDomainEventPublisher events) { _events = events; } public async Task HandleAsync(CreateUserCommand command, CancellationToken ct = default) { var userId = 123; // persist user await _events.PublishAsync(new UserCreatedEvent { UserId = userId, Email = command.Email }, ct); return userId; } } ``` ## 8. Saga Pattern Sagas orchestrate multi-step workflows with automatic compensation (rollback) on failure. Add the packages: ```bash dotnet add package Svrnty.CQRS.Sagas dotnet add package Svrnty.CQRS.Sagas.Abstractions dotnet add package Svrnty.CQRS.Sagas.RabbitMQ # for distributed sagas ``` ### Define Saga Data ```csharp using Svrnty.CQRS.Sagas.Abstractions; public class CreateOrderSagaData : ISagaData { public Guid CorrelationId { get; set; } public int OrderId { get; set; } public int PaymentId { get; set; } public decimal Amount { get; set; } } ``` ### Define a Saga ```csharp public class CreateOrderSaga : ISaga { public void Configure(ISagaBuilder builder) { builder .Step("CreateOrder") .Execute(async (data, ctx, ct) => { // Create the order data.OrderId = 42; }) .Compensate(async (data, ctx, ct) => { // Cancel the order on rollback }) .Then() .Step("ProcessPayment") .Execute(async (data, ctx, ct) => { // Charge payment data.PaymentId = 99; }) .Compensate(async (data, ctx, ct) => { // Refund payment on rollback }) .Then(); } } ``` ### Execute a Saga ```csharp using Svrnty.CQRS.Sagas.Abstractions; public class OrderCommandHandler : ICommandHandler { private readonly ISagaOrchestrator _orchestrator; public OrderCommandHandler(ISagaOrchestrator orchestrator) { _orchestrator = orchestrator; } public async Task HandleAsync(PlaceOrderCommand command, CancellationToken ct = default) { var state = await _orchestrator.StartAsync( new CreateOrderSagaData { Amount = command.Amount }, ct); // state.Status will be Completed or Compensated return state.Status == SagaStatus.Completed ? 1 : 0; } } ``` Saga statuses: `NotStarted` -> `InProgress` -> `Completed` (success) or `Failed` -> `Compensating` -> `Compensated` (rolled back). ### Remote Steps (Distributed Sagas) For steps that execute on remote services via RabbitMQ: ```csharp builder .SendCommand("ChargePayment") .WithCommand((data, ctx) => new ChargePaymentCommand { Amount = data.Amount }) .OnResponse(async (data, ctx, result, ct) => { data.PaymentId = result.PaymentId; }) .Compensate((data, ctx) => new RefundPaymentCommand { PaymentId = data.PaymentId }) .WithTimeout(TimeSpan.FromSeconds(30)) .WithRetry(maxRetries: 3, delay: TimeSpan.FromSeconds(2)) .Then(); ``` ## 9. Real-Time Notifications For pushing real-time updates to clients via gRPC streaming: ```bash dotnet add package Svrnty.CQRS.Notifications.Abstractions dotnet add package Svrnty.CQRS.Notifications.Grpc ``` ### Define a Notification ```csharp using Svrnty.CQRS.Notifications.Abstractions; [StreamingNotification(SubscriptionKey = "user-updates")] public record UserUpdatedNotification { public int UserId { get; init; } public string NewEmail { get; init; } = string.Empty; } ``` ### Publish a Notification ```csharp public class UpdateUserCommandHandler : ICommandHandler { private readonly INotificationPublisher _notifications; public UpdateUserCommandHandler(INotificationPublisher notifications) { _notifications = notifications; } public async Task HandleAsync(UpdateUserCommand command, CancellationToken ct = default) { // Update user... await _notifications.PublishAsync(new UserUpdatedNotification { UserId = command.UserId, NewEmail = command.NewEmail }, ct); } } ``` ## Running the Sample App The repository includes a complete sample application: ```bash cd Svrnty.Sample dotnet run ``` This starts: - gRPC server on `http://localhost:6000` (HTTP/2) - HTTP API on `http://localhost:6001` (HTTP/1.1) - Swagger UI at `http://localhost:6001/swagger` The sample demonstrates commands with validation, queries, gRPC reflection, MinimalApi endpoints, and dynamic queries.