dotnet-cqrs/Svrnty.CQRS.Events.Abstractions/Schema/EventVersionAttribute.cs

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;
}
}