Last active
January 12, 2024 21:17
-
-
Save Cryptoc1/93ff00ca285f1778d269896f02140c72 to your computer and use it in GitHub Desktop.
Operations (CQRS-like service pattern)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Cryptoc1.Extensions.Operations.Abstractions; | |
public interface IOperation | |
{ | |
} | |
public interface IOperation<TResult> | |
{ | |
} | |
public interface IOperationInvoker | |
{ | |
Task Invoke( IOperation operation, CancellationToken cancellation = default ); | |
Task<TResult> Invoke<TResult>( IOperation<TResult> operation, CancellationToken cancellation = default ); | |
} | |
public interface IOperationHandler<TOperation> | |
where TOperation : IOperation | |
{ | |
Task Invoke( TOperation operation, CancellationToken cancellation ); | |
} | |
public interface IOperationHandler<TOperation, TResult> | |
where TOperation : IOperation<TResult> | |
{ | |
Task<TResult> Invoke( TOperation operation, CancellationToken cancellation ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Cryptoc1.Extensions.Operations; | |
internal sealed partial class OperationInvoker( IServiceProvider serviceProvider ) : IOperationInvoker | |
{ | |
private readonly IServiceProvider serviceProvider = serviceProvider; | |
public Task Invoke( IOperation operation, CancellationToken cancellation ) | |
{ | |
ArgumentNullException.ThrowIfNull( operation ); | |
var descriptor = serviceProvider.GetHandlerDescriptor( operation.GetType() ); | |
var handler = ActivatorUtilities.CreateInstance( serviceProvider, descriptor.HandlerType ); | |
return Unsafe.As<Task>( | |
descriptor.InvokeMethod.Invoke( handler, [ operation, cancellation ] )! ); | |
} | |
public Task<TResult> Invoke<TResult>( IOperation<TResult> operation, CancellationToken cancellation ) | |
{ | |
ArgumentNullException.ThrowIfNull( operation ); | |
var descriptor = serviceProvider.GetHandlerDescriptor( operation.GetType() ); | |
var handler = ActivatorUtilities.CreateInstance( serviceProvider, descriptor.HandlerType ); | |
return Unsafe.As<Task<TResult>>( | |
descriptor.InvokeMethod.Invoke( handler, [ operation, cancellation ] )! ); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Cryptoc1.Extensions.Operations; | |
public static class OperationServiceExtensions | |
{ | |
public static IServiceCollection AddOperationHandler<THandler>( this IServiceCollection services ) | |
where THandler : class | |
{ | |
ArgumentNullException.ThrowIfNull( services ); | |
services.TryAddSingleton<HandlerDescriptorFinder>(); | |
services.TryAddTransient<IOperationInvoker, OperationInvoker>(); | |
var handlerType = typeof( THandler ); | |
var descriptors = handlerType.GetInterfaces() | |
.Where( IsOperationHandlerInterface ) | |
.Select( type => new OperationHandlerDescriptor( handlerType, type.GetMethod( "Invoke" )!, type.GenericTypeArguments[ 0 ] ) ) | |
.Select( ServiceDescriptor.Singleton ) | |
.ToArray(); | |
if( descriptors.Length is 0 ) | |
{ | |
throw new ArgumentException( $"Given type does not implement {handlerType.Name}.", nameof( THandler ) ); | |
} | |
return services.Add( descriptors ); | |
static bool IsOperationHandlerInterface( Type type ) | |
{ | |
if( !type.IsGenericType ) | |
{ | |
return false; | |
} | |
var definition = type.GetGenericTypeDefinition(); | |
return definition == typeof( IOperationHandler<> ) || definition == typeof( IOperationHandler<,> ); | |
} | |
} | |
internal static OperationHandlerDescriptor GetHandlerDescriptor( this IServiceProvider serviceProvider, Type operationType ) | |
{ | |
ArgumentNullException.ThrowIfNull( serviceProvider ); | |
ArgumentNullException.ThrowIfNull( operationType ); | |
return serviceProvider.GetRequiredService<HandlerDescriptorFinder>() | |
.Find( operationType ) | |
?? throw new InvalidOperationException( $"An IOperationHandler for {operationType} has not been registered to the service provider." ); | |
} | |
private sealed class HandlerDescriptorFinder( IEnumerable<OperationHandlerDescriptor> descriptors ) | |
{ | |
private readonly Dictionary<Type, OperationHandlerDescriptor> descriptorsByOperationType = descriptors.ToDictionary( handler => handler.OperationType ); | |
public OperationHandlerDescriptor? Find( Type operationType ) | |
{ | |
if( descriptorsByOperationType.TryGetValue( operationType, out var descriptor ) ) | |
{ | |
return descriptor; | |
} | |
return null; | |
} | |
} | |
} | |
public sealed record class OperationHandlerDescriptor( Type HandlerType, MethodInfo InvokeMethod, Type OperationType ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An "operation" is an arbitrary unit-of-work. It encapsulate a reusable, deterministic & independent block of code that "does something" (Inversion of Control principle).
Example Usage
IOperationInvoker
to invoke the operation: