using System.Reflection; using Books.Api.Infrastructure; using EventFlow.Aggregates; using EventFlow.EventStores; using EventFlow.Subscribers; using Hangfire; namespace Books.Api.EventFlow.Infrastructure; #pragma warning disable CS9113 // Parameter is unread public class DispatchToSubscriberResilienceStrategy( ILogger logger, IScheduler scheduler, IServiceProvider serviceProvider, IEventJsonSerializer eventJsonSerializer) : IDispatchToSubscriberResilienceStrategy #pragma warning restore CS9113 { public Task BeforeHandleEventAsync( ISubscribe subscriberTo, IDomainEvent domainEvent, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task HandleEventFailedAsync( ISubscribe subscriberTo, IDomainEvent domainEvent, Exception exception, bool swallowException, CancellationToken cancellationToken) { var subscriberType = GetSubscriberType(subscriberTo); var eventType = domainEvent.EventType.Name; var aggregateId = domainEvent.GetIdentity()?.Value ?? "unknown"; logger.LogError(exception, "[RESILIENCE] Subscriber {SubscriberType} failed to handle event {EventType} for aggregate {AggregateId}", subscriberType.Name, eventType, aggregateId); try { var serializedEvent = eventJsonSerializer.Serialize(domainEvent); var domainEventInterface = domainEvent.GetType().GetTypeInfo() .GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDomainEvent<,,>)); if (domainEventInterface != null) { scheduler.EnqueueJob( job => job.RetryEventDispatchAsync( subscriberType.AssemblyQualifiedName!, domainEventInterface.AssemblyQualifiedName!, serializedEvent.SerializedData, serializedEvent.SerializedMetadata), TimeSpan.FromSeconds(30)); logger.LogWarning( "[RESILIENCE] Scheduled retry job for {SubscriberType} / {EventType} / {AggregateId}", subscriberType.Name, eventType, aggregateId); } } catch (Exception e) { logger.LogError(e, "[RESILIENCE] Failed to schedule retry for {SubscriberType} / {EventType}. " + "This event will NOT be retried and may cause data inconsistency.", subscriberType.Name, eventType); } return Task.CompletedTask; } public Task HandleEventSucceededAsync( ISubscribe subscriberTo, IDomainEvent domainEvent, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task BeforeDispatchToSubscribersAsync( IDomainEvent domainEvent, IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task DispatchToSubscribersSucceededAsync( IDomainEvent domainEvent, IReadOnlyCollection domainEvents, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task HandleDispatchToSubscribersFailedAsync( IDomainEvent domainEvent, IReadOnlyCollection domainEvents, Exception exception, CancellationToken cancellationToken) { return Task.FromResult(true); } private static Type GetSubscriberType(ISubscribe subscriberTo) { if (subscriberTo is ISubscribeDecorator decorator) { return GetSubscriberType(decorator.InnerInstance as ISubscribe ?? throw new InvalidOperationException("InnerInstance is not ISubscribe")); } return subscriberTo.GetType(); } } public interface ISubscribeDecorator { object InnerInstance { get; } } public class SubscriberRetryJob( ILogger logger, IServiceProvider serviceProvider, IEventJsonSerializer eventJsonSerializer) { [AutomaticRetry(Attempts = 3, DelaysInSeconds = [30, 60, 120])] public async Task RetryEventDispatchAsync( string subscriberTypeName, string domainEventTypeName, string serializedData, string serializedMetadata) { logger.LogInformation( "[RETRY] Retrying event dispatch to {SubscriberType}", subscriberTypeName); try { var domainEvent = eventJsonSerializer.Deserialize(serializedData, serializedMetadata); var subscriberType = Type.GetType(subscriberTypeName); if (subscriberType == null) { logger.LogError("[RETRY] Could not resolve subscriber type: {SubscriberType}", subscriberTypeName); return; } var subscriber = FindSubscriberInstance(subscriberType, domainEvent); if (subscriber == null) { logger.LogError("[RETRY] Could not find subscriber instance for {SubscriberType}", subscriberTypeName); return; } var domainEventType = domainEvent.GetType(); var handleAsyncMethod = subscriber.GetType().GetMethods() .FirstOrDefault(m => m.Name == "HandleAsync" && m.GetParameters().Any(p => p.ParameterType.IsAssignableFrom(domainEventType))); if (handleAsyncMethod != null) { await (Task)handleAsyncMethod.Invoke(subscriber, [domainEvent, CancellationToken.None])!; logger.LogInformation("[RETRY] Successfully retried event dispatch to {SubscriberType}", subscriberTypeName); } else { logger.LogError( "[RETRY] No matching HandleAsync method found for {SubscriberType} and {DomainEventType}", subscriberTypeName, domainEventTypeName); } } catch (Exception ex) { logger.LogError(ex, "[RETRY] Failed to retry event dispatch to {SubscriberType}", subscriberTypeName); throw; } } private object? FindSubscriberInstance(Type subscriberType, object domainEvent) { var interfaceType = ExtractSubscriberInterface(subscriberType, domainEvent); if (interfaceType == null) return null; var instances = serviceProvider.GetServices(interfaceType); return instances.FirstOrDefault(x => x?.GetType() == subscriberType || HasInnerType(x, subscriberType)); } private static Type? ExtractSubscriberInterface(Type subscriberType, object domainEvent) { var domainEventType = domainEvent.GetType(); var interfaces = subscriberType.GetInterfaces() .Where(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(ISubscribeAsynchronousTo<,,>) || i.GetGenericTypeDefinition() == typeof(ISubscribeSynchronousTo<,,>))); foreach (var iface in interfaces) { var genericArguments = iface.GetGenericArguments(); var eventGenericArgs = domainEventType.GenericTypeArguments; if (eventGenericArgs.Length >= 3 && genericArguments[2] == eventGenericArgs[2]) { return iface; } } return null; } private static bool HasInnerType(object? obj, Type concreteType) { if (obj == null) return false; var decoratorType = obj.GetType(); var field = decoratorType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(x => x.Name.Contains("inner", StringComparison.InvariantCultureIgnoreCase)); var property = decoratorType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(x => x.Name.Contains("inner", StringComparison.InvariantCultureIgnoreCase)); if (field?.GetValue(obj)?.GetType() == concreteType) return true; if (property?.GetValue(obj)?.GetType() == concreteType) return true; return false; } }