using System; using System.Linq; namespace Svrnty.CQRS.Events.Abstractions.Schema; /// /// Marks an event type with version information for schema evolution. /// /// /// /// This attribute enables automatic schema versioning and upcasting. /// Use it to track event evolution over time and specify upcast relationships. /// /// /// Example: /// /// // 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 /// }; /// } /// } /// /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] public sealed class EventVersionAttribute : Attribute { /// /// Gets the version number of this event schema. /// /// /// Version numbers should start at 1 and increment sequentially. /// Version 1 represents the initial event schema. /// public int Version { get; } /// /// Gets the type of the previous version this event can upcast from. /// /// /// /// Should be null for version 1 (initial version). /// For versions > 1, specify the immediate previous version. /// /// /// Multi-hop upcasting is automatic: V1 → V2 → V3 /// You only need to specify the immediate previous version. /// /// public Type? UpcastFrom { get; init; } /// /// Gets or sets the event type name used for schema identification. /// /// /// /// If not specified, defaults to the class name without version suffix. /// Example: "UserCreatedEventV2" → "UserCreatedEvent" /// /// /// All versions of the same event should use the same EventTypeName. /// /// public string? EventTypeName { get; init; } /// /// Initializes a new instance of the class. /// /// The version number (must be >= 1). /// Thrown if version is less than 1. public EventVersionAttribute(int version) { if (version < 1) throw new ArgumentOutOfRangeException(nameof(version), "Version must be >= 1."); Version = version; } /// /// Gets the normalized event type name from a CLR type. /// /// The CLR type of the event. /// The normalized event type name (without version suffix). /// /// Removes common version suffixes: V1, V2, V3, etc. /// Example: "UserCreatedEventV2" → "UserCreatedEvent" /// 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; } }