Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

FunctionalUseCases

BuildNuGetLicense: MIT


A complete .NET solution that implements functional processing of use cases using the Mediator pattern with advanced ExecutionResult error handling. This library provides a clean way to organize business logic into discrete, testable use cases with sophisticated dependency injection support and functional error handling patterns.

Features

  • Mediator Pattern: Clean separation between use case parameters and their implementations
  • Dependency Injection: Full support for Microsoft.Extensions.DependencyInjection
  • Automatic Registration: Use Scrutor to automatically discover and register use cases
  • Advanced ExecutionResult Pattern: Functional approach with generic and non-generic variants
  • Rich Error Handling: ExecutionError with multiple messages, error codes, and log levels
  • Implicit Conversions: Seamless conversion between values and ExecutionResult
  • Result Combination: Combine multiple ExecutionResult objects using the + operator or Combine() method
  • Testable: Easy to unit test individual use cases with comprehensive error scenarios
  • Production Ready: Logging integration, cancellation support, and behavior pipeline
  • Execution Behaviors: Apply cross-cutting concerns globally or per-call (validation, logging, caching, transactions)
  • Use Case Chaining: Fluent chain execution with result passing and chain-aware behavior support

Installation

Add the required packages to your project:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Scrutor

Quick Start

1. Define a Use Case Parameter

usingFunctionalUseCases;publicclassGreetUserUseCase:IUseCaseParameter<string>{publicstringName{get;}publicGreetUserUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

2. Create a Use Case Implementation

usingFunctionalUseCases;publicclassGreetUserUseCaseHandler:IUseCase<GreetUserUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(GreetUserUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty");}vargreeting=$"Hello, {useCaseParameter.Name}!";returnExecution.Success(greeting);}}

3. Register Services

usingMicrosoft.Extensions.DependencyInjection;usingFunctionalUseCases;varservices=newServiceCollection();// Register all use cases from the assembly containing GreetUserUseCaseservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();varserviceProvider=services.BuildServiceProvider();

4. Execute Use Cases

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newGreetUserUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded){Console.WriteLine(result.CheckedValue);// Output: Hello, World!}else{Console.WriteLine($"Error: {result.Error?.Message}");}

Core Components

IUseCaseParameter Interface

Marker interface for use case parameters. All use case parameters should implement IUseCaseParameter<TResult>:

publicinterfaceIUseCaseParameter<outTResult>:IUseCaseParameter{}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

Generic interface for use case implementations that process use case parameters:

publicinterfaceIUseCase<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult and ExecutionResult

Advanced functional result types that encapsulate success/failure with rich error information:

// Generic variantpublicrecordExecutionResult<T>(ExecutionError?Error=null):ExecutionResult(Error)whereT:notnull{publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicTCheckedValue{get;}// Throws ExecutionException if failedpublicTGetValueOrThrow(string?exceptionMessage=null);publicTResultMatch<TResult>(Func<T,TResult>onSuccess,Func<ExecutionError,TResult>onFailure);publicExecutionResult<TResult>Map<TResult>(Func<T,TResult>map);publicExecutionResult<TResult>Bind<TResult>(Func<T,ExecutionResult<TResult>>bind);}// Non-generic variantpublicrecordExecutionResult(ExecutionError?Error=null){publicboolExecutionSucceeded{get;}publicboolExecutionFailed{get;}publicExecutionErrorCheckedError{get;}}// Factory methods via Execution classvarsuccess=Execution.Success("Hello World");varfailure=Execution.Failure<string>("Something went wrong");varfailureWithException=Execution.Failure<string>("Error message",exception);// Implicit conversionExecutionResult<string>result="Hello World";// Automatically creates success result

ExecutionError

Rich error information with support for multiple messages, error codes, and logging levels:

publicrecordExecutionError:ExecutionError<string>;publicrecordExecutionError<T>{publicstringMessage{get;}publicIList<T>Messages{get;set;}publicstring?ErrorCode{get;set;}publicLogLevelLogLevel{get;set;}publicException?Exception{get;set;}publicIDictionary<string,object?>Properties{get;set;}}

Exceptions passed to Execution.Failure(...) remain available through ExecutionError.Exception, including original type and stack trace.

IUseCaseDispatcher

Mediator that resolves and executes use cases:

publicinterfaceIUseCaseDispatcher{Task<ExecutionResult<TResult>>ExecuteAsync<TResult>(IUseCaseParameter<TResult>useCaseParameter,CancellationTokencancellationToken=default)whereTResult:notnull;}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Global Execution Behaviors

Global execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around all use case executions in a clean, composable way and are registered globally via dependency injection.

IExecutionBehavior Interface

publicinterfaceIExecutionBehavior<inTUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{Task<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default);}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

usingMicrosoft.Extensions.Logging;publicclassLoggingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyILogger<LoggingBehavior<TUseCaseParameter,TResult>>_logger;publicLoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter,TResult>>logger){_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varuseCaseParameterName=typeof(TUseCaseParameter).Name;_logger.LogInformation("Starting execution of use case: {UseCaseParameterName}",useCaseParameterName);varstopwatch=System.Diagnostics.Stopwatch.StartNew();try{varresult=awaitnext().ConfigureAwait(false);stopwatch.Stop();if(result.ExecutionSucceeded){_logger.LogInformation("Successfully executed use case: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);}else{_logger.LogWarning("Use case execution failed: {UseCaseParameterName} in {ElapsedMilliseconds}ms. Error: {ErrorMessage}",useCaseParameterName,stopwatch.ElapsedMilliseconds,result.Error?.Message);}returnresult;}catch(Exceptionex){stopwatch.Stop();_logger.LogError(ex,"Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms",useCaseParameterName,stopwatch.ElapsedMilliseconds);returnExecution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}",ex);}}}

Manual Registration

Global execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually and they will be applied to all use case executions:

// Register use cases from assemblyservices.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();// Register global execution behaviors manually - these apply to ALL use case executionsservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TimingBehavior<,>));

Execution Order

Global behaviors are executed in the order they are registered. Each behavior's ExecuteAsync is invoked once and receives the next delegate in the pipeline. Any code that runs before calling next() executes ahead of downstream steps, and any code that runs after awaiting next() executes after those steps complete:

Behavior 1 enters → Behavior 2 enters → Use Case Handler → Behavior 2 continues → Behavior 1 continues

Common Global Execution Behavior Patterns

Validation Behavior:

publicclassValidationBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){// Perform validation logicif(/* validation fails */){returnExecution.Failure<TResult>("Validation failed");}returnawaitnext().ConfigureAwait(false);}}

Caching Behavior:

publicclassCachingBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyIMemoryCache_cache;publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){varcacheKey=$"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";if(_cache.TryGetValue(cacheKey,outExecutionResult<TResult>cachedResult)){returncachedResult;}varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){_cache.Set(cacheKey,result,TimeSpan.FromMinutes(5));}returnresult;}}

Transaction Behavior:

publicclassTransactionBehavior<TUseCaseParameter,TResult>:IExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger<TransactionBehavior<TUseCaseParameter,TResult>>_logger;publicTransactionBehavior(ITransactionManagertransactionManager,ILogger<TransactionBehavior<TUseCaseParameter,TResult>>logger){_transactionManager=transactionManager;_logger=logger;}publicasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){ITransaction?transaction=null;try{// Begin transactiontransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);// Execute the use casevarresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){// Commit transaction on successawaittransaction.CommitAsync(cancellationToken);}else{// Rollback transaction on failureawaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch(Exceptionex){// Rollback transaction on exceptionif(transaction!=null){try{awaittransaction.RollbackAsync(cancellationToken);}catch(ExceptionrollbackEx){_logger.LogError(rollbackEx,"Failed to rollback transaction");// Don't throw rollback exception, preserve original exception}}returnExecution.Failure<TResult>($"Exception in TransactionBehavior: {ex.Message}",ex);}finally{// Ensure transaction is disposedtransaction?.Dispose();}}}

To use the transaction behavior, implement the ITransactionManager interface for your specific database technology:

// Example Entity Framework implementationpublicclassEntityFrameworkTransactionManager:ITransactionManager{privatereadonlyDbContext_context;publicEntityFrameworkTransactionManager(DbContextcontext){_context=context;}publicasyncTask<ITransaction>BeginTransactionAsync(CancellationTokencancellationToken=default){vartransaction=await_context.Database.BeginTransactionAsync(cancellationToken);returnnewEntityFrameworkTransaction(transaction);}}publicclassEntityFrameworkTransaction:ITransaction{privatereadonlyIDbContextTransaction_transaction;publicEntityFrameworkTransaction(IDbContextTransactiontransaction){_transaction=transaction;}publicasyncTaskCommitAsync(CancellationTokencancellationToken=default){await_transaction.CommitAsync(cancellationToken);}publicasyncTaskRollbackAsync(CancellationTokencancellationToken=default){await_transaction.RollbackAsync(cancellationToken);}publicvoidDispose(){_transaction.Dispose();}}// Register the transaction behavior and managerservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddScoped(typeof(IExecutionBehavior<,>),typeof(TransactionBehavior<,>));

Located in: FunctionalUseCases/TransactionBehavior.cs and FunctionalUseCases/Interfaces/ITransactionManager.cs

Per-Call Execution Behaviors (WithBehavior API)

In addition to global behaviors that apply to all use case executions, the library provides a powerful fluent API for applying behaviors to specific use case executions or chains. This allows for fine-grained control over when and where behaviors are applied.

Two Types of Behaviors

The system now supports two distinct behavior application patterns:

  1. Global Behaviors: Registered with dependency injection and applied to ALL use case executions
  2. Per-Call Behaviors: Applied to specific executions using the WithBehavior() fluent API with open generic types

WithBehavior() Fluent API

The WithBehavior() method allows you to apply behaviors to specific use case executions using open generic type definitions. This approach ensures behaviors remain cross-cutting concerns that work with any use case parameter and result types.

Single Use Case with Behavior

// Apply a transaction behavior to a specific use case executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Apply multiple behaviors to the same executionvarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).ExecuteAsync(newMyUseCase("data"));// Use behavior instances instead of typesvarcustomBehavior=newCustomBehavior<MyUseCase,string>(someParameter);varresult=awaitdispatcher.WithBehavior(customBehavior).ExecuteAsync(newMyUseCase("data"));

Use Case Chains with Behaviors

// Apply behavior to an entire use case chainvarresult=awaitdispatcher.StartWith(newFirstUseCase("initial")).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newSecondUseCase(x.Id,x.Property)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();// Apply multiple behaviors to a chainvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).WithBehavior(typeof(TransactionBehavior<,>)).WithBehavior(typeof(ValidationBehavior<,>)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Behaviors can be added at any point in the chainvarresult=awaitdispatcher.StartWith(newFirstUseCase()).Then(x =>newSecondUseCase(x.Id)).WithBehavior(typeof(TransactionBehavior<,>)).Then(x =>newThirdUseCase(x.ProcessedData)).ExecuteAsync();

Chain-Aware Transaction Behavior

The TransactionBehavior<TUseCaseParameter, TResult> is a sophisticated example of a chain-aware behavior that adapts its strategy based on the execution context:

Intelligent Transaction Management

  • Single Use Case: Creates transaction at use case start → commits/rollbacks at use case end
  • Chain Execution: Creates transaction at chain start → commits/rollbacks at chain end
  • Automatic Detection: Uses IExecutionScope to determine context without user intervention

Example Transaction Behavior Usage

// Transaction per single use casevarresult=awaitdispatcher.WithBehavior(typeof(TransactionBehavior<,>)).ExecuteAsync(newCreateOrderUseCase(orderData));// Creates transaction → executes use case → commits/rollbacks transaction// Transaction per entire chainvarresult=awaitdispatcher.StartWith(newCreateOrderUseCase(orderData)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(inventory =>newProcessPaymentUseCase(orderData.Payment)).Then(payment =>newSendConfirmationEmailUseCase(order.CustomerEmail)).ExecuteAsync();// Creates transaction → executes entire chain → commits/rollbacks transaction

Creating Chain-Aware Behaviors

To create behaviors that adapt to execution context, implement IScopedExecutionBehavior<TUseCaseParameter, TResult> instead of the base IExecutionBehavior<TUseCaseParameter, TResult>:

usingMicrosoft.Extensions.Logging;publicclassCustomTransactionBehavior<TUseCaseParameter,TResult>:ScopedExecutionBehavior<TUseCaseParameter,TResult>whereTUseCaseParameter:IUseCaseParameter<TResult>whereTResult:notnull{privatereadonlyITransactionManager_transactionManager;privatereadonlyILogger_logger;publicCustomTransactionBehavior(ITransactionManagertransactionManager,ILoggerlogger){_transactionManager=transactionManager;_logger=logger;}publicoverrideasyncTask<ExecutionResult<TResult>>ExecuteAsync(TUseCaseParameteruseCaseParameter,IExecutionScopescope,PipelineBehaviorDelegate<TResult>next,CancellationTokencancellationToken=default){if(scope.IsChainExecution){// Chain execution logicif(scope.IsChainStart){_logger.LogInformation("Starting transaction for chain {ChainId}",scope.ChainId);// Start transaction for entire chain}varresult=awaitnext().ConfigureAwait(false);if(scope.IsChainEnd){// Commit or rollback transaction at chain endif(result.ExecutionSucceeded){_logger.LogInformation("Committing transaction for chain {ChainId}",scope.ChainId);// Commit transaction}else{_logger.LogWarning("Rolling back transaction for chain {ChainId}",scope.ChainId);// Rollback transaction}}returnresult;}else{// Single use case execution logic_logger.LogInformation("Starting transaction for single use case");// Create transaction → execute → commit/rollbackvartransaction=await_transactionManager.BeginTransactionAsync(cancellationToken);try{varresult=awaitnext().ConfigureAwait(false);if(result.ExecutionSucceeded){awaittransaction.CommitAsync(cancellationToken);}else{awaittransaction.RollbackAsync(cancellationToken);}returnresult;}catch{awaittransaction.RollbackAsync(cancellationToken);throw;}finally{transaction.Dispose();}}}}

ExecutionScope Interface

The IExecutionScope interface provides context information to chain-aware behaviors:

publicinterfaceIExecutionScope{boolIsChainExecution{get;}// True if part of a use case chainboolIsChainStart{get;}// True if first use case in chainboolIsChainEnd{get;}// True if last use case in chainstring?ChainId{get;}// Unique identifier for the chain}

Behavior Registration for Per-Call Usage

Per-call behaviors are registered as open generic types and resolved at execution time based on the actual use case parameter and result types:

// Register behaviors as open generics for per-call usageservices.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped(typeof(CachingBehavior<,>));// Register any dependencies they needservices.AddScoped<ITransactionManager,EntityFrameworkTransactionManager>();services.AddMemoryCache();// For caching behavior// Global behaviors are still registered the same wayservices.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));

Key Benefits

  1. Selective Application: Apply expensive behaviors (like transactions) only where needed
  2. Chain-Aware Intelligence: Behaviors automatically adapt to single vs. chain execution
  3. Composition: Combine multiple per-call behaviors for specific scenarios
  4. Performance: Avoid overhead of global behaviors when not needed
  5. Flexibility: Mix global and per-call behaviors as appropriate

Use Case Examples

Scenario 1: E-commerce Order Processing

// Transaction behavior applied to entire order workflowvarresult=awaitdispatcher.StartWith(newValidateOrderUseCase(orderRequest)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newReserveInventoryUseCase(order.Items)).Then(reservation =>newProcessPaymentUseCase(reservation.OrderId,orderRequest.Payment)).Then(payment =>newCreateOrderUseCase(payment.OrderId,payment.Amount)).ExecuteAsync();// Single transaction spans the entire workflow

Scenario 2: Caching Expensive Queries

// Cache only expensive user profile queriesvarprofile=awaitdispatcher.WithBehavior(typeof(CachingBehavior<,>)).ExecuteAsync(newGetUserProfileUseCase(userId));// Regular user operations don't use cachingvarupdateResult=awaitdispatcher.ExecuteAsync(newUpdateUserNameUseCase(userId,newName));

Scenario 3: Validation for Critical Operations

// Apply strict validation only to sensitive operationsvarresult=awaitdispatcher.WithBehavior(typeof(StrictValidationBehavior<,>)).WithBehavior(typeof(AuditLogBehavior<,>)).ExecuteAsync(newDeleteAccountUseCase(userId,confirmationToken));

Use Case Chaining

The library provides powerful use case chaining capabilities that allow you to compose multiple use cases into a sequential workflow. Results are automatically passed between use cases, and execution stops on the first failure.

Basic Chain Syntax

// Chain multiple use cases with result passingvarresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newValidateUserUseCase(user)).Then(user =>newSendWelcomeEmailUseCase(user.Email,user.Name)).ExecuteAsync();// Access the final resultif(result.ExecutionSucceeded){Console.WriteLine($"Welcome email sent: {result.CheckedValue}");}

Result Passing Between Use Cases

The Then() method automatically passes the result of the previous use case to the next:

varresult=awaitdispatcher.StartWith(newCreateUserUseCase("John","john@example.com")).Then(user =>newAssignRoleUseCase(user.Id,"StandardUser")).Then(userRole =>newSendActivationEmailUseCase(userRole.User.Email,userRole.ActivationToken)).Then(activation =>newLogUserCreationUseCase(activation.UserId,activation.Timestamp)).ExecuteAsync();// Each use case receives the .CheckedValue from the previous use case as its parameter

Error Handling in Chains

Chains stop execution on the first failure and provide comprehensive error handling:

varresult=awaitdispatcher.StartWith(newValidateInputUseCase(inputData)).Then(validInput =>newProcessDataUseCase(validInput)).Then(processedData =>newSaveDataUseCase(processedData)).OnError(error =>{// Handle any error that occurred in the chainlogger.LogError("Chain execution failed: {Error}",error.Message);returnTask.FromResult(Execution.Failure<SavedData>($"Processing failed: {error.Message}"));}).ExecuteAsync();// If any step fails, the OnError handler is called and subsequent steps are skipped

Combining Chains with Behaviors

Chains work seamlessly with both global and per-call behaviors:

// Apply transaction behavior to entire chainvarresult=awaitdispatcher.StartWith(newBeginOrderUseCase(customerId)).WithBehavior(typeof(TransactionBehavior<,>)).Then(order =>newAddItemsUseCase(order.Id,items)).Then(order =>newCalculateTotalUseCase(order)).Then(order =>newProcessPaymentUseCase(order.Total,paymentInfo)).ExecuteAsync();// Global logging behavior will still apply to all steps// Transaction behavior will create one transaction for the entire chain

Advanced Chain Patterns

Conditional Execution:

varresult=awaitdispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>user.IsActive?newSendNotificationUseCase(user.Id,message):newLogInactiveUserUseCase(user.Id)).ExecuteAsync();

Parallel Processing (using multiple chains):

// Execute multiple independent chainsvaruserTask=dispatcher.StartWith(newGetUserUseCase(userId)).Then(user =>newUpdateLastLoginUseCase(user.Id)).ExecuteAsync();varpreferencesTask=dispatcher.StartWith(newGetUserPreferencesUseCase(userId)).Then(prefs =>newApplyThemeUseCase(prefs.ThemeId)).ExecuteAsync();// Wait for both chains to completevaruserResult=awaituserTask;varpreferencesResult=awaitpreferencesTask;

Chain Branching:

varresult=awaitdispatcher.StartWith(newProcessOrderUseCase(orderId)).Then(order =>order.IsExpress?dispatcher.StartWith(newExpressShippingUseCase(order)).Then(shipping =>newSendExpressNotificationUseCase(shipping)).ExecuteAsync():dispatcher.StartWith(newStandardShippingUseCase(order)).Then(shipping =>newSendStandardNotificationUseCase(shipping)).ExecuteAsync()).ExecuteAsync();

Registration Options

The library provides several extension methods for registering use cases (located in: FunctionalUseCases/Extensions/UseCaseRegistrationExtensions.cs). Registration recap:

// Register use casesservices.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();// Global execution behaviors (manual, applied to all executions)services.AddScoped(typeof(IExecutionBehavior<,>),typeof(LoggingBehavior<,>));// Per-call behaviors for WithBehavior() (open generics resolved at execution time)services.AddScoped(typeof(TransactionBehavior<,>));services.AddScoped(typeof(ValidationBehavior<,>));services.AddScoped<CachingBehavior<GetUserUseCase,User>>();

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success resultExecutionResult<string>result="Hello World";// Explicit failure creationvarfailure=Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)varresult1=Execution.Success();varresult2=Execution.Failure("Something went wrong");varcombined=result1+result2;// Will be failure with error message// Multiple operationsvarsuccess1=Execution.Success("Value1");varsuccess2=Execution.Success("Value2");varfailure1=Execution.Failure<string>("Error1");varallCombined=success1+success2+failure1;// Will be failure with "Error1"// Using the Combine method directlyvarcombined=Execution.Combine(result1,result2,result3);

Error Handling Patterns

varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);// Pattern 1: Check success and access valueif(result.ExecutionSucceeded){varvalue=result.GetValueOrThrow();Console.WriteLine(value);}// Pattern 2: Handle failureif(result.ExecutionFailed){varerror=result.Error;Console.WriteLine($"Error: {error?.Message}");// Access additional error informationConsole.WriteLine($"Error Code: {error?.ErrorCode}");Console.WriteLine($"Log Level: {error?.LogLevel}");if(error?.Exception!=null){Console.WriteLine($"Exception: {error.Exception.Message}");}}// Pattern 3: Throw on failureresult.ThrowIfFailed("Custom error message");// Pattern 4: Functional compositionvardisplayName=result.Map(value =>value.ToString()).Bind(value =>string.IsNullOrWhiteSpace(value)?Execution.Failure<string>("Display name is empty","EMPTY_DISPLAY_NAME"):Execution.Success(value)).Match(value =>value, error =>$"Failed: {error.Message}");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Loggingvarresult=Execution.Failure<string>("Database connection failed",errorCode:"DB_001",logLevel:LogLevel.Critical);// Use logging extension. Preserved exceptions are passed to ILogger.result.Log(logger);

ASP.NET Core Mapping

Install optional FunctionalUseCases.AspNetCore package to map results without adding ASP.NET Core dependencies to core package:

usingFunctionalUseCases.AspNetCore;returnresult.ToActionResult();

Failures become RFC-style ProblemDetails. Numeric HTTP error codes map directly; domain codes can provide Properties["statusCode"] or a custom ExecutionResultHttpOptions.StatusCodeSelector. Exception details remain hidden unless IncludeExceptionDetails is enabled.

Example Use Cases

The library includes a comprehensive sample implementation demonstrating the pattern:

  • SampleUseCase: Use case parameter containing a name for greeting generation
  • SampleUseCaseHandler: Use case implementation that processes the parameter with validation and business logic using ExecutionResult API

Run the sample application to see it in action:

cd Sample
dotnet run

Sample Implementation

Use Case Parameter:

publicclassSampleUseCase:IUseCaseParameter<string>{publicstringName{get;}publicSampleUseCase(stringname){Name=name??thrownewArgumentNullException(nameof(name));}}

Use Case Implementation:

publicclassSampleUseCaseHandler:IUseCase<SampleUseCase,string>{publicasyncTask<ExecutionResult<string>>ExecuteAsync(SampleUseCaseuseCaseParameter,CancellationTokencancellationToken=default){if(string.IsNullOrWhiteSpace(useCaseParameter.Name)){returnExecution.Failure<string>("Name cannot be empty or whitespace");}vargreeting=$"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";returnExecution.Success(greeting);}}

Usage:

vardispatcher=serviceProvider.GetRequiredService<IUseCaseDispatcher>();varuseCaseParameter=newSampleUseCase("World");varresult=awaitdispatcher.ExecuteAsync(useCaseParameter);if(result.ExecutionSucceeded)Console.WriteLine(result.CheckedValue);// "Hello, World! Welcome to FunctionalUseCases."elseConsole.WriteLine(result.Error?.Message);

Project Structure

FunctionalUseCases/
├── FunctionalUseCases.sln # Solution file
├── FunctionalUseCases/ # Main library
│ ├── ExecutionResult.cs # Result types (generic & non-generic)
│ ├── Execution.cs # Factory methods
│ ├── ExecutionError.cs # Error types
│ ├── ExecutionException.cs # Exception type
│ ├── UseCaseDispatcher.cs # Mediator implementation with execution behavior support
│ ├── PipelineBehaviorDelegate.cs # Execution behavior delegate type
│ ├── Interfaces/ # All interfaces
│ │ ├── IUseCase.cs # Use case parameter and implementation interfaces
│ │ ├── IUseCaseDispatcher.cs # Dispatcher interface
│ │ └── IExecutionBehavior.cs # Execution behavior interface
│ ├── Extensions/ # Extension methods
│ │ ├── ExecutionResultExtensions.cs # Logging & utility extensions
│ │ └── UseCaseRegistrationExtensions.cs # DI extensions (manual behavior registration required)
│ └── Sample/ # Sample implementation
│ ├── SampleUseCase.cs # Example use case parameter
│ ├── SampleUseCaseHandler.cs # Example use case implementation
│ └── LoggingBehavior.cs # Example execution behavior
├── Sample/ # Console application
│ └── Program.cs # Demo application with execution behaviors
└── README.md # This file

Building and Testing

# Build the solution
dotnet build
# Run the samplecd Sample && dotnet run
# Run tests (if available)
dotnet test

Sample Output with Execution Behaviors:

=== FunctionalUseCases Sample Application with Execution Behaviors ===
Example 1: Successful execution
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 103ms
Success: Hello, World! Welcome to FunctionalUseCases.
Example 2: Failed execution (empty name)
info: Starting execution of use case: SampleUseCase -> String
warn: Use case execution failed: SampleUseCase -> String in 101ms. Error: Name cannot be empty or whitespace
Error: Name cannot be empty or whitespace
Example 3: Use Case Chain
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 98ms
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 95ms
Chain Success: Hello, SecondStep-9! Welcome to FunctionalUseCases.
Example 6: Interactive
Enter your name: Alice
info: Starting execution of use case: SampleUseCase -> String
info: Successfully executed use case: SampleUseCase -> String in 92ms
Interactive Success: Hello, Alice! Welcome to FunctionalUseCases.

Examples 4 and 5 demonstrate the WithBehavior() API. Register a per-call behavior such as TransactionBehavior<,> (as shown in the registration section) before running them to see the behavior wrap the execution or the entire chain. If the behavior is not registered, the DI container will throw a missing-service error, highlighting the need to register open generic behaviors explicitly.

Best Practices

  1. Keep Use Case Parameters Simple: Each use case parameter should represent a single business operation's input data
  2. Immutable Use Case Parameters: Make use case parameter properties read-only for thread safety
  3. Validation in Use Cases: Perform validation in use case implementations, not in use case parameters
  4. Rich Error Handling: Use ExecutionResult with specific error codes and appropriate log levels
  5. Async Operations: Always use async/await for potentially long-running operations
  6. Cancellation Support: Support cancellation tokens for responsive applications
  7. Meaningful Names: Use descriptive names that clearly indicate the business operation being performed
  8. Single Responsibility: Each use case should handle one specific business scenario
  9. Global vs Per-Call Behaviors: Use global behaviors for cross-cutting concerns that apply everywhere (logging, monitoring). Use per-call behaviors for context-specific operations (transactions, validation, caching)
  10. Behavior Registration: Remember to manually register both global and per-call execution behaviors as they are not automatically discovered
  11. Chain Design: Design use case chains to be atomic units of work - if any step fails, the entire operation should be considered failed
  12. Result Passing: Structure use case parameters to accept the exact data they need from previous use cases in chains
  13. Transaction Scope: Use TransactionBehavior on chains rather than individual use cases when you need atomic operations across multiple steps
  14. Chain-Aware Behaviors: Implement IScopedExecutionBehavior when creating behaviors that need to adapt based on execution context

Interface Naming

The library uses clear, intent-revealing interface names:

  • IUseCaseParameter: Represents the data/parameters for a use case
  • IUseCase: Represents the actual use case implementation/logic
  • IExecutionBehavior: Represents cross-cutting behavior that wraps use case execution
  • ExecuteAsync: Method name that clearly indicates execution of business logic

This naming convention follows the principle that parameters define what data is needed, while use cases define how that data is processed, and behaviors define how execution is enhanced.

Versioning

This library uses semantic versioning powered by Nerdbank.GitVersioning:

  • Automatic version generation from Git history
  • NuGet packages aligned with repository versions
  • Runtime version information available via assembly attributes
  • Ready for CI/CD pipelines

Version Information Access

// Access version information at runtimevarassembly=typeof(Execution).Assembly;varversion=assembly.GetName().Version;varinformationalVersion=assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;// Example output: "1.0.1+136a4d399f" (includes Git commit hash)Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection (10.0.0)
  • Microsoft.Extensions.Logging.Abstractions (10.0.0) - For rich error handling and logging
  • Scrutor (5.0.1) - For automatic service registration
  • Nerdbank.GitVersioning (3.7.115) - For semantic versioning

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

Functional processing of use cases using Mediator pattern

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages