134 lines
4.4 KiB
C#
134 lines
4.4 KiB
C#
using System;
|
|
using System.Linq;
|
|
|
|
namespace Svrnty.CQRS.Events.Abstractions.Schema;
|
|
|
|
/// <summary>
|
|
/// Marks an event type with version information for schema evolution.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This attribute enables automatic schema versioning and upcasting.
|
|
/// Use it to track event evolution over time and specify upcast relationships.
|
|
/// </para>
|
|
/// <para>
|
|
/// <strong>Example:</strong>
|
|
/// <code>
|
|
/// // Version 1 (initial)
|
|
/// [EventVersion(1)]
|
|
/// public record UserCreatedEventV1 : CorrelatedEvent
|
|
/// {
|
|
/// public string Name { get; init; }
|
|
/// }
|
|
///
|
|
/// // Version 2 (added Email property)
|
|
/// [EventVersion(2, UpcastFrom = typeof(UserCreatedEventV1))]
|
|
/// public record UserCreatedEventV2 : CorrelatedEvent
|
|
/// {
|
|
/// public string Name { get; init; }
|
|
/// public string Email { get; init; }
|
|
///
|
|
/// // Static upcaster method (convention-based)
|
|
/// public static UserCreatedEventV2 UpcastFrom(UserCreatedEventV1 v1)
|
|
/// {
|
|
/// return new UserCreatedEventV2
|
|
/// {
|
|
/// EventId = v1.EventId,
|
|
/// CorrelationId = v1.CorrelationId,
|
|
/// OccurredAt = v1.OccurredAt,
|
|
/// Name = v1.Name,
|
|
/// Email = "unknown@example.com" // Default value for new property
|
|
/// };
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </para>
|
|
/// </remarks>
|
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
|
|
public sealed class EventVersionAttribute : Attribute
|
|
{
|
|
/// <summary>
|
|
/// Gets the version number of this event schema.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Version numbers should start at 1 and increment sequentially.
|
|
/// Version 1 represents the initial event schema.
|
|
/// </remarks>
|
|
public int Version { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the type of the previous version this event can upcast from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Should be null for version 1 (initial version).
|
|
/// For versions > 1, specify the immediate previous version.
|
|
/// </para>
|
|
/// <para>
|
|
/// Multi-hop upcasting is automatic: V1 → V2 → V3
|
|
/// You only need to specify the immediate previous version.
|
|
/// </para>
|
|
/// </remarks>
|
|
public Type? UpcastFrom { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the event type name used for schema identification.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// If not specified, defaults to the class name without version suffix.
|
|
/// Example: "UserCreatedEventV2" → "UserCreatedEvent"
|
|
/// </para>
|
|
/// <para>
|
|
/// All versions of the same event should use the same EventTypeName.
|
|
/// </para>
|
|
/// </remarks>
|
|
public string? EventTypeName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="EventVersionAttribute"/> class.
|
|
/// </summary>
|
|
/// <param name="version">The version number (must be >= 1).</param>
|
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if version is less than 1.</exception>
|
|
public EventVersionAttribute(int version)
|
|
{
|
|
if (version < 1)
|
|
throw new ArgumentOutOfRangeException(nameof(version), "Version must be >= 1.");
|
|
|
|
Version = version;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the normalized event type name from a CLR type.
|
|
/// </summary>
|
|
/// <param name="eventType">The CLR type of the event.</param>
|
|
/// <returns>The normalized event type name (without version suffix).</returns>
|
|
/// <remarks>
|
|
/// Removes common version suffixes: V1, V2, V3, etc.
|
|
/// Example: "UserCreatedEventV2" → "UserCreatedEvent"
|
|
/// </remarks>
|
|
public static string GetEventTypeName(Type eventType)
|
|
{
|
|
var attribute = eventType.GetCustomAttributes(typeof(EventVersionAttribute), false)
|
|
.FirstOrDefault() as EventVersionAttribute;
|
|
|
|
if (attribute?.EventTypeName != null)
|
|
return attribute.EventTypeName;
|
|
|
|
// Remove version suffix (V1, V2, etc.) from type name
|
|
var typeName = eventType.Name;
|
|
var versionSuffixIndex = typeName.LastIndexOf('V');
|
|
|
|
if (versionSuffixIndex > 0 && versionSuffixIndex < typeName.Length - 1)
|
|
{
|
|
var suffix = typeName.Substring(versionSuffixIndex + 1);
|
|
if (int.TryParse(suffix, out _))
|
|
{
|
|
return typeName.Substring(0, versionSuffixIndex);
|
|
}
|
|
}
|
|
|
|
return typeName;
|
|
}
|
|
}
|