基于类型的Simple Injector条件注册及MediatR多服务充值实现问询
Great question! Let’s walk through how to set this up cleanly for your phone customer recharge use case—where you need to execute different recharge commands based on the services a customer has activated.
1. Define Core Interfaces for Recharge Commands
First, establish a base set of interfaces to standardize your recharge commands and their handlers. This keeps your Command pattern consistent and scalable:
// Base command interface for all recharge operations public interface IRechargeCommand { Guid CustomerId { get; } decimal Amount { get; } } // Generic handler interface tied to specific recharge commands public interface IRechargeCommandHandler<in TCommand> where TCommand : IRechargeCommand { Task HandleAsync(TCommand command, CancellationToken cancellationToken); }
2. Implement Service-Specific Commands & Handlers
Create concrete implementations for each service type (e.g., voice, data, SMS). These encapsulate the unique logic for recharging each service:
// Voice service recharge command public class VoiceRechargeCommand : IRechargeCommand { public Guid CustomerId { get; init; } public decimal Amount { get; init; } // Add service-specific properties (like call minute allocation) if needed } // Voice service recharge handler public class VoiceRechargeHandler : IRechargeCommandHandler<VoiceRechargeCommand> { public async Task HandleAsync(VoiceRechargeCommand command, CancellationToken cancellationToken) { // Logic to process voice service recharge (e.g., update customer's call balance) await Console.Out.WriteLineAsync($"Recharging voice service for customer {command.CustomerId} with ${command.Amount}"); } } // Repeat for other services public class DataRechargeCommand : IRechargeCommand { /* ... */ } public class DataRechargeHandler : IRechargeCommandHandler<DataRechargeCommand> { /* ... */ }
3. Configure Simple Injector Conditional Registration
Use Simple Injector’s RegisterConditional method to map each command type to its exact corresponding handler. This ensures the right handler is resolved when you request it for a specific command:
var container = new Container(); // Register all IRechargeCommandHandler implementations from your assembly container.Register(typeof(IRechargeCommandHandler<>), typeof(VoiceRechargeHandler).Assembly); // Add conditional registration to enforce exact command-handler matching container.RegisterConditional( typeof(IRechargeCommandHandler<>), typeof(IRechargeCommandHandler<>), context => context.Consumer.ImplementationType.GetGenericArguments()[0] == context.ServiceType.GetGenericArguments()[0] ); // Register MediatR with Simple Injector (standard setup) container.RegisterMediatR(typeof(YourMediatRHandlerMarkerClass).Assembly); // Validate container configuration to catch mismatches early container.Verify();
This conditional registration guarantees that when you request IRechargeCommandHandler<VoiceRechargeCommand>, Simple Injector resolves only VoiceRechargeHandler—not any other handler that implements the generic interface.
4. Use Handlers in Your MediatR Request Processor
In your main MediatR handler (the one that loads the customer’s activated services), inject a factory or enumerable of handlers to resolve and execute the correct logic for each service:
Option 1: Use a Factory for Precise Resolution
public class ProcessCustomerRechargeHandler : IRequestHandler<ProcessCustomerRechargeRequest, Unit> { private readonly ICustomerServiceRepository _customerServiceRepo; private readonly Func<Type, IRechargeCommandHandler<IRechargeCommand>> _handlerFactory; public ProcessCustomerRechargeHandler( ICustomerServiceRepository customerServiceRepo, Func<Type, IRechargeCommandHandler<IRechargeCommand>> handlerFactory) { _customerServiceRepo = customerServiceRepo; _handlerFactory = handlerFactory; } public async Task<Unit> Handle(ProcessCustomerRechargeRequest request, CancellationToken cancellationToken) { // Load the customer's activated services var activatedServices = await _customerServiceRepo.GetActivatedServicesAsync(request.CustomerId, cancellationToken); foreach (var service in activatedServices) { // Create the appropriate recharge command based on service type IRechargeCommand command = service.Type switch { ServiceType.Voice => new VoiceRechargeCommand { CustomerId = request.CustomerId, Amount = request.Amount }, ServiceType.Data => new DataRechargeCommand { CustomerId = request.CustomerId, Amount = request.Amount }, _ => throw new NotSupportedException($"Service type {service.Type} isn't supported for recharge") }; // Resolve the matching handler var handlerType = typeof(IRechargeCommandHandler<>).MakeGenericType(command.GetType()); dynamic handler = _handlerFactory(handlerType); // Execute the recharge logic await handler.HandleAsync((dynamic)command, cancellationToken); } return Unit.Value; } }
Option 2: Use an Enumerable to Filter Handlers
If you prefer, inject all handlers and filter for the correct one instead of using a factory:
public class ProcessCustomerRechargeHandler : IRequestHandler<ProcessCustomerRechargeRequest, Unit> { private readonly ICustomerServiceRepository _customerServiceRepo; private readonly IEnumerable<IRechargeCommandHandler<IRechargeCommand>> _allHandlers; public ProcessCustomerRechargeHandler( ICustomerServiceRepository customerServiceRepo, IEnumerable<IRechargeCommandHandler<IRechargeCommand>> allHandlers) { _customerServiceRepo = customerServiceRepo; _allHandlers = allHandlers; } public async Task<Unit> Handle(ProcessCustomerRechargeRequest request, CancellationToken cancellationToken) { var activatedServices = await _customerServiceRepo.GetActivatedServicesAsync(request.CustomerId, cancellationToken); foreach (var service in activatedServices) { IRechargeCommand command = /* ... create command as before ... */; var handlerType = typeof(IRechargeCommandHandler<>).MakeGenericType(command.GetType()); // Find the handler that matches the command type var handler = _allHandlers.First(h => handlerType.IsAssignableFrom(h.GetType())); dynamic typedHandler = handler; await typedHandler.HandleAsync((dynamic)command, cancellationToken); } return Unit.Value; } }
Key Tips
- Type Safety: Using
dynamichere is safe because you control the command-handler mapping via your switch statement and conditional registration. For stricter safety, you could add a method to handlers that returns their supported command type. - Lifetimes: If your handlers depend on scoped services (like database contexts), register them with
Lifestyle.Scopedto match your MediatR setup. - Testing: You can easily mock the handler factory or enumerable in unit tests to isolate your main processor logic.
内容的提问来源于stack exchange,提问作者Lorenzo

