1+ using System . Text . Json . Serialization ;
2+
13namespace ModularityKit . Mutator . Abstractions . Effects ;
24
35/// <summary>
46/// Represents a side effect produced by a mutation.
57/// Side effects capture additional consequences that are not part of the primary state change.
68/// </summary>
9+ [ JsonConverter ( typeof ( SideEffectJsonConverter ) ) ]
710public sealed class SideEffect
811{
912 /// <summary>
@@ -28,6 +31,16 @@ public sealed class SideEffect
2831 /// </summary>
2932 public object ? Data { get ; init ; }
3033
34+ /// <summary>
35+ /// Stable contract identifier for typed side effect payloads.
36+ /// </summary>
37+ public string ? DataContractType { get ; init ; }
38+
39+ /// <summary>
40+ /// Version number for typed side effect payloads.
41+ /// </summary>
42+ public int ? DataContractVersion { get ; init ; }
43+
3144 /// <summary>
3245 /// Timestamp when the side effect occurred.
3346 /// </summary>
@@ -43,7 +56,10 @@ public sealed class SideEffect
4356 /// </summary>
4457 /// <param name="type">The type of the side effect.</param>
4558 /// <param name="description">Human-readable description.</param>
46- /// <param name="data">Optional associated data.</param>
59+ /// <param name="data">
60+ /// Optional associated data. When the payload type declares <see cref="SideEffectDataContractAttribute"/>,
61+ /// the side effect contract metadata is populated automatically.
62+ /// </param>
4763 /// <param name="severity">Severity level.</param>
4864 /// <param name="requiresAction">
4965 /// Indicates whether the side effect requires explicit follow-up. Critical severity always implies action.
@@ -56,22 +72,47 @@ public static SideEffect Create(
5672 SideEffectSeverity severity = SideEffectSeverity . Info ,
5773 bool requiresAction = false ,
5874 DateTimeOffset ? timestamp = null )
59- => new ( )
60- {
61- Type = type ,
62- Description = description ,
63- Data = data ,
64- Severity = severity ,
65- RequiresAction = requiresAction || severity == SideEffectSeverity . Critical ,
66- Timestamp = timestamp ?? DateTimeOffset . UtcNow
67- } ;
75+ => CreateCore (
76+ type ,
77+ description ,
78+ data ,
79+ severity ,
80+ requiresAction ,
81+ timestamp ) ;
82+
83+ /// <summary>
84+ /// Creates a new <see cref="SideEffect"/> with a typed payload contract.
85+ /// </summary>
86+ /// <typeparam name="TData">The payload type.</typeparam>
87+ /// <param name="type">The type of the side effect.</param>
88+ /// <param name="description">Human-readable description.</param>
89+ /// <param name="data">Typed associated payload.</param>
90+ /// <param name="severity">Severity level.</param>
91+ /// <param name="requiresAction">
92+ /// Indicates whether the side effect requires explicit follow-up. Critical severity always implies action.
93+ /// </param>
94+ /// <param name="timestamp">Optional timestamp override. Defaults to current UTC time.</param>
95+ public static SideEffect Create < TData > (
96+ string type ,
97+ string description ,
98+ TData data ,
99+ SideEffectSeverity severity = SideEffectSeverity . Info ,
100+ bool requiresAction = false ,
101+ DateTimeOffset ? timestamp = null )
102+ {
103+ ArgumentNullException . ThrowIfNull ( data ) ;
104+ return CreateCore ( type , description , data , severity , requiresAction , timestamp ) ;
105+ }
68106
69107 /// <summary>
70108 /// Creates a new critical <see cref="SideEffect"/> instance.
71109 /// </summary>
72110 /// <param name="type">The type of the side effect.</param>
73111 /// <param name="description">Human-readable description.</param>
74- /// <param name="data">Optional associated data.</param>
112+ /// <param name="data">
113+ /// Optional associated data. When the payload type declares <see cref="SideEffectDataContractAttribute"/>,
114+ /// the side effect contract metadata is populated automatically.
115+ /// </param>
75116 /// <param name="timestamp">Optional timestamp override. Defaults to current UTC time.</param>
76117 public static SideEffect Critical (
77118 string type ,
@@ -85,4 +126,83 @@ public static SideEffect Critical(
85126 SideEffectSeverity . Critical ,
86127 requiresAction : true ,
87128 timestamp : timestamp ) ;
129+
130+ /// <summary>
131+ /// Creates a new critical <see cref="SideEffect"/> instance with a typed payload contract.
132+ /// </summary>
133+ /// <typeparam name="TData">The payload type.</typeparam>
134+ /// <param name="type">The type of the side effect.</param>
135+ /// <param name="description">Human-readable description.</param>
136+ /// <param name="data">Typed associated payload.</param>
137+ /// <param name="timestamp">Optional timestamp override. Defaults to current UTC time.</param>
138+ public static SideEffect Critical < TData > (
139+ string type ,
140+ string description ,
141+ TData data ,
142+ DateTimeOffset ? timestamp = null )
143+ => Create (
144+ type ,
145+ description ,
146+ data ,
147+ SideEffectSeverity . Critical ,
148+ requiresAction : true ,
149+ timestamp : timestamp ) ;
150+
151+ /// <summary>
152+ /// Attempts to read the side effect payload as a typed contract.
153+ /// </summary>
154+ /// <typeparam name="TData">The expected payload type.</typeparam>
155+ /// <param name="data">The typed payload when available.</param>
156+ /// <returns><see langword="true"/> when the payload is available as <typeparamref name="TData"/>.</returns>
157+ public bool TryGetData < TData > ( out TData ? data )
158+ {
159+ if ( Data is TData typed )
160+ {
161+ data = typed ;
162+ return true ;
163+ }
164+
165+ data = default ;
166+ return false ;
167+ }
168+
169+ private static SideEffect CreateCore (
170+ string type ,
171+ string description ,
172+ object ? data ,
173+ SideEffectSeverity severity ,
174+ bool requiresAction ,
175+ DateTimeOffset ? timestamp )
176+ {
177+ var ( contractType , contractVersion ) = ResolveContract ( data ) ;
178+
179+ return new SideEffect
180+ {
181+ Type = type ,
182+ Description = description ,
183+ Data = data ,
184+ Severity = severity ,
185+ RequiresAction = requiresAction || severity == SideEffectSeverity . Critical ,
186+ Timestamp = timestamp ?? DateTimeOffset . UtcNow ,
187+ DataContractType = contractType ,
188+ DataContractVersion = contractVersion
189+ } ;
190+ }
191+
192+ private static ( string ? ContractType , int ? ContractVersion ) ResolveContract ( object ? data )
193+ {
194+ if ( data is null )
195+ return ( null , null ) ;
196+
197+ var dataType = data . GetType ( ) ;
198+ var contract = dataType . GetCustomAttributes ( typeof ( SideEffectDataContractAttribute ) , inherit : false )
199+ . OfType < SideEffectDataContractAttribute > ( )
200+ . SingleOrDefault ( ) ;
201+
202+ if ( contract is null )
203+ return ( null , null ) ;
204+
205+ SideEffectDataContractRegistry . Register ( dataType ) ;
206+ return ( contract . ContractType , contract . ContractVersion ) ;
207+ }
88208}
0 commit comments