154 lines
5.2 KiB
C#
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
|
|
};
|
|
}
|
|
}
|