dotnet-cqrs/Svrnty.Sample/Events/VersionedUserEvents.cs

154 lines
5.2 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Models;
using Svrnty.CQRS.Events.Abstractions.Schema;
using Svrnty.CQRS.Events.Abstractions.EventStore;
using Svrnty.CQRS.Events.Abstractions;
namespace Svrnty.Sample.Events;
// ============================================================================
// PHASE 5: EVENT VERSIONING DEMONSTRATION
// ============================================================================
// This file demonstrates event schema evolution with automatic upcasting.
// Shows how to evolve event schemas over time without breaking compatibility.
// ============================================================================
/// <summary>
/// Version 1 of UserCreatedEvent (initial schema).
/// Original design with a single "FullName" field.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Version 1 Schema:</strong>
/// - UserId: int
/// - FullName: string (combined first and last name)
/// </para>
/// </remarks>
[EventVersion(1)]
public sealed record UserCreatedEventV1 : CorrelatedEvent
{
public required int UserId { get; init; }
public required string FullName { get; init; }
}
/// <summary>
/// Version 2 of UserCreatedEvent (evolved schema).
/// Improved design that separates name components and adds email.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Version 2 Schema:</strong>
/// - UserId: int
/// - FirstName: string (split from FullName)
/// - LastName: string (split from FullName)
/// - Email: string (new field)
/// </para>
/// <para>
/// <strong>Schema Changes from V1:</strong>
/// - FullName → FirstName + LastName (split transformation)
/// - Added Email field (default: "unknown@example.com")
/// </para>
/// <para>
/// <strong>Automatic Upcasting:</strong>
/// When a V1 event is consumed by a subscription configured for V2,
/// the framework automatically calls <see cref="UpcastFrom"/> to transform it.
/// </para>
/// </remarks>
[EventVersion(2, UpcastFrom = typeof(UserCreatedEventV1))]
public sealed record UserCreatedEventV2 : CorrelatedEvent
{
public required int UserId { get; init; }
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required string Email { get; init; }
/// <summary>
/// Convention-based upcaster: Transforms V1 events to V2.
/// </summary>
/// <remarks>
/// <para>
/// The framework discovers this method automatically via reflection.
/// Method signature must match: <c>public static {ToType} UpcastFrom({FromType})</c>
/// </para>
/// <para>
/// <strong>Transformation Logic:</strong>
/// - Split FullName on first space
/// - First part becomes FirstName
/// - Remaining parts become LastName
/// - Email defaults to "unknown@example.com" (data not available in V1)
/// - Preserve correlation metadata (EventId, CorrelationId, OccurredAt)
/// </para>
/// </remarks>
public static UserCreatedEventV2 UpcastFrom(UserCreatedEventV1 v1)
{
// Split full name into components
var parts = v1.FullName.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var firstName = parts.Length > 0 ? parts[0] : "Unknown";
var lastName = parts.Length > 1 ? parts[1] : "";
return new UserCreatedEventV2
{
// Preserve correlation metadata from V1
EventId = v1.EventId,
CorrelationId = v1.CorrelationId,
OccurredAt = v1.OccurredAt,
// Transform data fields
UserId = v1.UserId,
FirstName = firstName,
LastName = lastName,
Email = "unknown@example.com" // Default for new field
};
}
}
/// <summary>
/// Version 3 of UserCreatedEvent (further evolution).
/// Adds phone number and makes email optional.
/// </summary>
/// <remarks>
/// <para>
/// <strong>Version 3 Schema:</strong>
/// - UserId: int
/// - FirstName: string
/// - LastName: string
/// - Email: string (now nullable)
/// - PhoneNumber: string? (new optional field)
/// </para>
/// <para>
/// <strong>Multi-Hop Upcasting:</strong>
/// The framework can automatically upcast V1 → V2 → V3 by chaining upcasters.
/// You only need to define V1→V2 and V2→V3; the framework handles V1→V3.
/// </para>
/// </remarks>
[EventVersion(3, UpcastFrom = typeof(UserCreatedEventV2))]
public sealed record UserCreatedEventV3 : CorrelatedEvent
{
public required int UserId { get; init; }
public required string FirstName { get; init; }
public required string LastName { get; init; }
public string? Email { get; init; }
public string? PhoneNumber { get; init; }
/// <summary>
/// Upcaster: Transforms V2 events to V3.
/// </summary>
public static UserCreatedEventV3 UpcastFrom(UserCreatedEventV2 v2)
{
return new UserCreatedEventV3
{
// Preserve correlation metadata
EventId = v2.EventId,
CorrelationId = v2.CorrelationId,
OccurredAt = v2.OccurredAt,
// Copy existing fields
UserId = v2.UserId,
FirstName = v2.FirstName,
LastName = v2.LastName,
Email = v2.Email != "unknown@example.com" ? v2.Email : null,
PhoneNumber = null // New field, no data available from V2
};
}
}