191 lines
6.0 KiB
C#
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; }
|
|
}
|