Skip to content

Instantly share code, notes, and snippets.

@Cryptoc1
Last active January 12, 2024 21:17
Show Gist options
  • Save Cryptoc1/93ff00ca285f1778d269896f02140c72 to your computer and use it in GitHub Desktop.
Save Cryptoc1/93ff00ca285f1778d269896f02140c72 to your computer and use it in GitHub Desktop.
Operations (CQRS-like service pattern)
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 );
}
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 ] )! );
}
};
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 );
@Cryptoc1
Copy link
Author

Cryptoc1 commented Jan 12, 2024

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

  • Define operation+handler types:
public static class UserOperation
{
  public sealed record GetById( int UserId ) : IOperation<User?>;
}

internal sealed class GetUserHandler( MyDbContext context ) : IOperationHandler<UserOperation.GetById, User?>
{
  public async Task<User> Invoke( UserOperation.GetById operation, CancellationToken cancellation )
  {
     ArgumentNullException.ThrowIfNull( operation );

    // get user from db 
    var entity = await context.Users.FindAsync( [operation.UserId], cancellation );
    
    return ...;
  }
}
  • Register handle as a service
var services = new ServiceCollection()
  .AddOperationHandler<GetUserHandler>();
  • Use IOperationInvoker to invoke the operation:
public async Task<IActionResult> Get( [FromServices] IOperationInvoker operations, [FromRoute] int id )
{
  return Ok(
    await operations.Invoke( new UserOperation.GetById( id ) ) );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment