dotnet-cqrs/Svrnty.Sample/Workflows/InviteUserWorkflow.cs

191 lines
6.0 KiB
C#

using System;
using Svrnty.Sample.Events;
using Svrnty.CQRS.Events.Abstractions.EventHandlers;
using Svrnty.CQRS.Events.Abstractions.Models;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using Svrnty.CQRS.Events.Abstractions;
namespace Svrnty.Sample.Workflows;
// ============================================================================
// STEP 1: Invite User Command
// ============================================================================
/// <summary>
/// Command to invite a user via email.
/// This is the first step in a multi-step workflow.
/// </summary>
public record InviteUserCommand
{
public required string Email { get; init; }
public required string InviterName { get; init; }
}
public class InviteUserCommandValidator : AbstractValidator<InviteUserCommand>
{
public InviteUserCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("Valid email is required");
RuleFor(x => x.InviterName)
.NotEmpty()
.WithMessage("Inviter name is required");
}
}
/// <summary>
/// Handler for inviting a user.
/// Phase 1: Creates a new workflow instance for each invitation.
/// </summary>
/// <remarks>
/// Future phases will support workflow continuation where Accept/Decline commands
/// can reference the same workflow instance using the workflow ID.
/// </remarks>
public class InviteUserCommandHandler : ICommandHandlerWithWorkflow<InviteUserCommand, string, InvitationWorkflow>
{
public async Task<string> HandleAsync(
InviteUserCommand command,
InvitationWorkflow workflow,
CancellationToken cancellationToken = default)
{
// Generate invitation ID
var invitationId = Guid.NewGuid().ToString();
// Emit event via workflow
// Framework automatically assigns workflow.Id as CorrelationId
workflow.EmitInvited(new UserInvitedEvent
{
InvitationId = invitationId,
Email = command.Email,
InviterName = command.InviterName
});
// Return invitation ID (client can use this to accept/decline)
// Note: workflow.Id is the correlation ID, but for Phase 1 they're independent
return await Task.FromResult(invitationId);
}
}
// ============================================================================
// STEP 2: Accept/Decline Invite Commands
// ============================================================================
/// <summary>
/// Command to accept a user invitation.
/// Uses the same business data (email) as correlation key.
/// </summary>
public record AcceptInviteCommand
{
public required string InvitationId { get; init; }
public required string Email { get; init; } // Used for correlation
public required string Name { get; init; }
}
public class AcceptInviteCommandValidator : AbstractValidator<AcceptInviteCommand>
{
public AcceptInviteCommandValidator()
{
RuleFor(x => x.InvitationId)
.NotEmpty()
.WithMessage("Invitation ID is required");
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required");
}
}
/// <summary>
/// Handler for accepting an invitation.
/// Phase 1: Creates a new workflow instance for the acceptance.
/// </summary>
/// <remarks>
/// Future phases will support continuing the InvitationWorkflow from the invite command,
/// so all events (invite + accept) share the same workflow/correlation ID.
/// </remarks>
public class AcceptInviteCommandHandler : ICommandHandlerWithWorkflow<AcceptInviteCommand, int, InvitationWorkflow>
{
public async Task<int> HandleAsync(
AcceptInviteCommand command,
InvitationWorkflow workflow,
CancellationToken cancellationToken = default)
{
// Generate user ID
var userId = new Random().Next(1000, 9999);
// Emit acceptance event via workflow
workflow.EmitAccepted(new UserInviteAcceptedEvent
{
InvitationId = command.InvitationId,
UserId = userId,
Name = command.Name
});
return await Task.FromResult(userId);
}
}
/// <summary>
/// Command to decline a user invitation.
/// </summary>
public record DeclineInviteCommand
{
public required string InvitationId { get; init; }
public string? Reason { get; init; }
}
/// <summary>
/// Handler for declining an invitation.
/// Phase 1: Creates a new workflow instance for the decline action.
/// </summary>
/// <remarks>
/// Future phases will support continuing the InvitationWorkflow from the invite command,
/// so all events (invite + decline) share the same workflow/correlation ID.
/// </remarks>
public class DeclineInviteCommandHandler : ICommandHandlerWithWorkflow<DeclineInviteCommand, InvitationWorkflow>
{
public async Task HandleAsync(
DeclineInviteCommand command,
InvitationWorkflow workflow,
CancellationToken cancellationToken = default)
{
// Emit decline event via workflow
workflow.EmitDeclined(new UserInviteDeclinedEvent
{
InvitationId = command.InvitationId,
Reason = command.Reason
});
await Task.CompletedTask;
}
}
// ============================================================================
// Events for the Workflow
// ============================================================================
public sealed record UserInvitedEvent : UserEvent
{
public required string InvitationId { get; init; }
public required string Email { get; init; }
public required string InviterName { get; init; }
}
public sealed record UserInviteAcceptedEvent : UserEvent
{
public required string InvitationId { get; init; }
public required int UserId { get; init; }
public required string Name { get; init; }
}
public sealed record UserInviteDeclinedEvent : UserEvent
{
public required string InvitationId { get; init; }
public string? Reason { get; init; }
}