-
Notifications
You must be signed in to change notification settings - Fork 145
Description
Allow Custom Registration of INotificationHandler without Duplicate Registrations
Problem Description
Currently, when using Mediator, developers cannot register their INotificationHandler<T> implementations manually without causing duplicate service registrations. If you register a handler yourself and then call services.AddMediator(...), the handler gets registered twice in the DI container.
Additionally, calling services.AddMediator() first and then attempting to manually register handlers doesn't work either, because Mediator uses factory functions (GetRequiredService<T>()) rather than direct type registrations for notification handlers.
Current Behavior
// Scenario 1: Manual registration first (causes duplicates)
services.AddScoped<INotificationHandler<MyNotification>, MyNotificationHandler>();
services.AddMediator(); // Results in duplicate registration
// Scenario 2: AddMediator first (doesn't work due to factory pattern)
services.AddMediator();
services.AddScoped<INotificationHandler<MyNotification>, MyNotificationHandler>(); // Again duplicate registration
// Scenario 3: Attempt to override after AddMediator (impossible due to factory registration)
services.AddMediator();
// Try to find and remove auto-registration to replace with custom lifetime
var existingHandler = services.FirstOrDefault(s =>
s.ServiceType == typeof(INotificationHandler<MyNotification>) &&
s.ImplementationType == typeof(MyNotificationHandler)); // This returns null!
// existingHandler is null because ImplementationType is null for factory registrations
// The actual registration uses: sp => sp.GetRequiredService<MyNotificationHandler>()Proposed Solutions
I see two potential approaches to resolve this issue:
Option 1: Roslyn Analyzer (Less Preferred)
Create a Roslyn analyzer that emits compilation errors when developers attempt to manually register INotificationHandler<T> implementations.
Option 2: Smart Registration Check (Preferred)
Modify the Mediator registration logic to check for existing handler registrations before adding new ones.
Preferred Implementation
My preferred solution would implement the following behavior:
- Allow manual handler registration with any desired lifetime (Transient, Scoped, Singleton)
- Smart duplicate detection in
services.AddMediator(...):- Check if a service for the given handler type and implementation type is already registered
- If already registered → ignore it
- If not registered → register with default lifetime
Code Examples
Desired Usage Pattern
public class MyNotificationHandler : INotificationHandler<OrderCreated>
{
public Task Handle(OrderCreated notification, CancellationToken cancellationToken)
{
// Handle the notification
return Task.CompletedTask;
}
}
// In Program.cs or Startup.cs
services.AddSingleton<INotificationHandler<OrderCreated>, MyNotificationHandler>(); // Custom lifetime
services.AddScoped<INotificationHandler<CustomerUpdated>, CustomerUpdatedHandler>(); // Custom lifetime
// This should not duplicate the manually registered handlers
services.AddMediator(options =>
{
options.DefaultHandlerLifetime = ServiceLifetime.Transient; // Default for auto-registered handlers
});Implementation Logic (Based on Generated Code Analysis)
Looking at the actual generated code, the current registration pattern for notification handlers is:
// 1. Register the concrete handler implementation
services.TryAdd(new ServiceDescriptor(typeof(MyNotificationHandler), typeof(MyNotificationHandler), ServiceLifetime.Singleton));
// 2. Register the interface using a factory that resolves the concrete type
services.Add(new ServiceDescriptor(typeof(INotificationHandler<MyNotification>),
sp => sp.GetRequiredService<MyNotificationHandler>(), ServiceLifetime.Singleton));The proposed smart registration logic would need to:
/// <summary>
/// Enhanced registration method that respects existing handler registrations
/// while maintaining factory pattern compatibility
/// </summary>
public static IServiceCollection AddMediator(this IServiceCollection services, Action<MediatorOptions> configure = null)
{
var options = new MediatorOptions();
configure?.Invoke(options);
// Register core Mediator services first...
RegisterMediatorCore(services);
// Build cache of existing registrations for efficient lookup
var existingRegistrations = BuildRegistrationCache(services);
foreach (var handlerInfo in discoveredHandlers)
{
var registrationKey = new ServiceRegistrationKey(handlerInfo.ConcreteType, handlerInfo.ConcreteType);
if (!existingRegistrations.TryGetValue(registrationKey, out var existingDescriptor))
{
// Register concrete handler with default lifetime if not already present
services.TryAdd(new ServiceDescriptor(
handlerInfo.ConcreteType,
handlerInfo.ConcreteType,
options.DefaultHandlerLifetime));
existingDescriptor = new ServiceDescriptor(handlerInfo.ConcreteType, handlerInfo.ConcreteType, options.DefaultHandlerLifetime);
}
// If already registered, respect the existing registration and its lifetime
// Always register the interface mapping using factory pattern
// This overwrites any direct interface registrations (which wouldn't work anyway)
services.Add(new ServiceDescriptor(
handlerInfo.InterfaceType,
sp => sp.GetRequiredService(handlerInfo.ConcreteType),
existingDescriptor.Lifetime));
}
return services;
}
/// <summary>
/// Builds a dictionary cache of existing service registrations for efficient lookup
/// </summary>
/// <param name="services">The service collection to cache</param>
/// <returns>Dictionary with ServiceRegistrationKey as key and ServiceDescriptor as value</returns>
private static Dictionary<ServiceRegistrationKey, ServiceDescriptor> BuildRegistrationCache(IServiceCollection services)
{
var cache = new Dictionary<ServiceRegistrationKey, ServiceDescriptor>();
foreach (var service in services)
{
if (service.ImplementationType != null)
{
var key = new ServiceRegistrationKey(service.ServiceType, service.ImplementationType);
cache[key] = service;
}
}
return cache;
}
/// <summary>
/// Composite key for service registration lookup combining ServiceType and ImplementationType
/// </summary>
private readonly struct ServiceRegistrationKey : IEquatable<ServiceRegistrationKey>
{
public Type ServiceType { get; }
public Type ImplementationType { get; }
public ServiceRegistrationKey(Type serviceType, Type implementationType)
{
ServiceType = serviceType;
ImplementationType = implementationType;
}
public bool Equals(ServiceRegistrationKey other) =>
ServiceType == other.ServiceType && ImplementationType == other.ImplementationType;
public override bool Equals(object obj) =>
obj is ServiceRegistrationKey other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(ServiceType, ImplementationType);
}Benefits
- Resolves Issue [Suggestion] Option to control handler lifetime #60: Provides flexibility for custom handler lifetimes while maintaining default behavior
- Developer Control: Allows explicit lifetime management for specific handlers
- Backward Compatibility: Existing code continues to work unchanged
- Clean Registration: No duplicate services in the DI container
- Performance Optimized: Uses dictionary caching for efficient registration lookups
Disadvantages
- Performance: since we need to create a dictionary and loop through all handlers, the startup performance would be worse of course. But maybe this feature could be configured using a new "AddMediator" option. (i.e.
bool AllowExplicitHandlerRegistrationsIf that would be false, the roslyn analyzer that provides a build error if you'd register handlers yourself would again come in handy ;-)
Questions
- Is this approach technically feasible with the current architecture?
- Would you prefer a different detection mechanism for existing registrations?
- Should there be any configuration options to control this behavior?
This enhancement would significantly improve the developer experience when working with custom handler lifetimes and DI container management.