FunctionalUseCases 1.0.4-ga4ddd48266

This is a prerelease version of FunctionalUseCases.
dotnet add package FunctionalUseCases --version 1.0.4-ga4ddd48266
                    
NuGet\Install-Package FunctionalUseCases -Version 1.0.4-ga4ddd48266
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FunctionalUseCases" Version="1.0.4-ga4ddd48266" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FunctionalUseCases" Version="1.0.4-ga4ddd48266" />
                    
Directory.Packages.props
<PackageReference Include="FunctionalUseCases" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FunctionalUseCases --version 1.0.4-ga4ddd48266
                    
#r "nuget: FunctionalUseCases, 1.0.4-ga4ddd48266"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FunctionalUseCases@1.0.4-ga4ddd48266
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FunctionalUseCases&version=1.0.4-ga4ddd48266&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FunctionalUseCases&version=1.0.4-ga4ddd48266&prerelease
                    
Install as a Cake Tool

FunctionalUseCases

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: Sophisticated functional approach with both 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
  • ๐Ÿ“ฆ Enterprise-Ready: Robust implementation with logging integration and cancellation support
  • ๐Ÿ”— Execution Behaviors: Cross-cutting concerns like logging, validation, caching, and performance monitoring through a clean execution behavior pattern

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

using FunctionalUseCases;

public class GreetUserUseCase : IUseCaseParameter<string>
{
    public string Name { get; }

    public GreetUserUseCase(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }
}

2. Create a Use Case Implementation

using FunctionalUseCases;

public class GreetUserUseCaseHandler : IUseCase<GreetUserUseCase, string>
{
    public async Task<ExecutionResult<string>> ExecuteAsync(GreetUserUseCase useCaseParameter, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(useCaseParameter.Name))
        {
            return Execution.Failure<string>("Name cannot be empty");
        }

        var greeting = $"Hello, {useCaseParameter.Name}!";
        return Execution.Success(greeting);
    }
}

3. Register Services

using Microsoft.Extensions.DependencyInjection;
using FunctionalUseCases;

var services = new ServiceCollection();

// Register all use cases from the assembly containing GreetUserUseCase
services.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();

var serviceProvider = services.BuildServiceProvider();

4. Execute Use Cases

var dispatcher = serviceProvider.GetRequiredService<IUseCaseDispatcher>();

var useCaseParameter = new GreetUserUseCase("World");
var result = await dispatcher.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>:

public interface IUseCaseParameter<out TResult> : IUseCaseParameter
{
}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

IUseCase Interface

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

public interface IUseCase<in TUseCaseParameter, TResult>
    where TUseCaseParameter : IUseCaseParameter<TResult>
    where TResult : notnull
{
    Task<ExecutionResult<TResult>> ExecuteAsync(TUseCaseParameter useCaseParameter, CancellationToken cancellationToken = default);
}

Located in: FunctionalUseCases/Interfaces/IUseCase.cs

ExecutionResult<T> and ExecutionResult

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

// Generic variant
public record ExecutionResult<T>(ExecutionError? Error = null) : ExecutionResult(Error) where T : notnull
{
    public bool ExecutionSucceeded { get; }
    public bool ExecutionFailed { get; }
    public T CheckedValue { get; } // Throws if failed
}

// Non-generic variant
public record ExecutionResult(ExecutionError? Error = null)
{
    public bool ExecutionSucceeded { get; }
    public bool ExecutionFailed { get; }
    public ExecutionError CheckedError { get; }
}

// Factory methods via Execution class
var success = Execution.Success("Hello World");
var failure = Execution.Failure<string>("Something went wrong");
var failureWithException = Execution.Failure<string>("Error message", exception);

// Implicit conversion
ExecutionResult<string> result = "Hello World"; // Automatically creates success result

ExecutionError

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

public record ExecutionError(
    string Message,
    string? ErrorCode = null,
    LogLevel LogLevel = LogLevel.Error,
    Exception? Exception = null,
    IDictionary<string, object>? Properties = null
);

IUseCaseDispatcher

Mediator that resolves and executes use cases:

public interface IUseCaseDispatcher
{
    Task<ExecutionResult<TResult>> ExecuteAsync<TResult>(IUseCaseParameter<TResult> useCaseParameter, CancellationToken cancellationToken = default)
        where TResult : notnull;
}

Located in: FunctionalUseCases/Interfaces/IUseCaseDispatcher.cs

Execution Behaviors

Execution behaviors allow you to implement cross-cutting concerns like logging, validation, caching, performance monitoring, and more. They wrap around use case execution in a clean, composable way.

IExecutionBehavior Interface

public interface IExecutionBehavior<in TUseCaseParameter, TResult>
    where TUseCaseParameter : IUseCaseParameter<TResult>
    where TResult : notnull
{
    Task<ExecutionResult<TResult>> ExecuteAsync(TUseCaseParameter useCaseParameter, PipelineBehaviorDelegate<TResult> next, CancellationToken cancellationToken = default);
}

Located in: FunctionalUseCases/Interfaces/IExecutionBehavior.cs

Creating an Execution Behavior

using Microsoft.Extensions.Logging;

public class LoggingBehavior<TUseCaseParameter, TResult> : IExecutionBehavior<TUseCaseParameter, TResult>
    where TUseCaseParameter : IUseCaseParameter<TResult>
    where TResult : notnull
{
    private readonly ILogger<LoggingBehavior<TUseCaseParameter, TResult>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TUseCaseParameter, TResult>> logger)
    {
        _logger = logger;
    }

    public async Task<ExecutionResult<TResult>> ExecuteAsync(TUseCaseParameter useCaseParameter, PipelineBehaviorDelegate<TResult> next, CancellationToken cancellationToken = default)
    {
        var useCaseParameterName = typeof(TUseCaseParameter).Name;
        
        _logger.LogInformation("Starting execution of use case: {UseCaseParameterName}", useCaseParameterName);
        
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        
        try
        {
            var result = await next().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);
            }

            return result;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex, "Exception occurred during use case execution: {UseCaseParameterName} in {ElapsedMilliseconds}ms", 
                useCaseParameterName, stopwatch.ElapsedMilliseconds);
            
            return Execution.Failure<TResult>($"Exception in LoggingBehavior: {ex.Message}", ex);
        }
    }
}

Manual Registration

Execution behaviors are NOT automatically registered when you call the registration extension methods. You must register them manually:

// Register use cases from assembly
services.AddUseCasesFromAssemblyContaining<GreetUserUseCase>();

// Register execution behaviors manually
services.AddScoped(typeof(IExecutionBehavior<,>), typeof(LoggingBehavior<,>));
services.AddScoped(typeof(IExecutionBehavior<,>), typeof(TimingBehavior<,>));

Execution Order

Behaviors are executed in the order they are registered. Each behavior can execute logic before and after the next step in the pipeline:

Behavior 1 (before) โ†’ Behavior 2 (before) โ†’ Use Case Handler โ†’ Behavior 2 (after) โ†’ Behavior 1 (after)

Common Execution Behavior Patterns

Validation Behavior:

public class ValidationBehavior<TUseCaseParameter, TResult> : IExecutionBehavior<TUseCaseParameter, TResult>
    where TUseCaseParameter : IUseCaseParameter<TResult>
    where TResult : notnull
{
    public async Task<ExecutionResult<TResult>> ExecuteAsync(TUseCaseParameter useCaseParameter, PipelineBehaviorDelegate<TResult> next, CancellationToken cancellationToken = default)
    {
        // Perform validation logic
        if (/* validation fails */)
        {
            return Execution.Failure<TResult>("Validation failed");
        }
        
        return await next().ConfigureAwait(false);
    }
}

Caching Behavior:

public class CachingBehavior<TUseCaseParameter, TResult> : IExecutionBehavior<TUseCaseParameter, TResult>
    where TUseCaseParameter : IUseCaseParameter<TResult>
    where TResult : notnull
{
    private readonly IMemoryCache _cache;

    public async Task<ExecutionResult<TResult>> ExecuteAsync(TUseCaseParameter useCaseParameter, PipelineBehaviorDelegate<TResult> next, CancellationToken cancellationToken = default)
    {
        var cacheKey = $"{typeof(TUseCaseParameter).Name}_{useCaseParameter.GetHashCode()}";
        
        if (_cache.TryGetValue(cacheKey, out ExecutionResult<TResult> cachedResult))
        {
            return cachedResult;
        }
        
        var result = await next().ConfigureAwait(false);
        
        if (result.ExecutionSucceeded)
        {
            _cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
        }
        
        return result;
    }
}

Registration Options

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

Note: Execution behaviors are NOT automatically registered. Register execution behaviors manually using standard DI registration:

// Register from specific assemblies
services.AddUseCases(new[] { typeof(MyUseCaseParameter).Assembly });

// Register from calling assembly
services.AddUseCasesFromAssembly();

// Register from assembly containing a specific type
services.AddUseCasesFromAssemblyContaining<MyUseCaseParameter>();

// Specify service lifetime (default is Transient)
services.AddUseCasesFromAssembly(ServiceLifetime.Scoped);

// Register execution behaviors manually
services.AddScoped(typeof(IExecutionBehavior<,>), typeof(LoggingBehavior<,>));

Advanced ExecutionResult Features

Implicit Conversions

// Implicit conversion from value to success result
ExecutionResult<string> result = "Hello World";

// Explicit failure creation
var failure = Execution.Failure<string>("Something went wrong");

Combining Results

// Using the + operator (new feature)
var result1 = Execution.Success();
var result2 = Execution.Failure("Something went wrong");
var combined = result1 + result2; // Will be failure with error message

// Multiple operations
var success1 = Execution.Success("Value1");
var success2 = Execution.Success("Value2");
var failure1 = Execution.Failure<string>("Error1");
var allCombined = success1 + success2 + failure1; // Will be failure with "Error1"

// Using the Combine method directly
var combined = Execution.Combine(result1, result2, result3);

Error Handling Patterns

var result = await dispatcher.ExecuteAsync(useCaseParameter);

// Pattern 1: Check success and access value
if (result.ExecutionSucceeded)
{
    var value = result.CheckedValue; // Safe access to value
    Console.WriteLine(value);
}

// Pattern 2: Handle failure
if (result.ExecutionFailed)
{
    var error = result.Error;
    Console.WriteLine($"Error: {error?.Message}");
    
    // Access additional error information
    Console.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 failure
result.ThrowIfFailed("Custom error message");

Logging Integration

// ExecutionResult integrates with Microsoft.Extensions.Logging
var result = Execution.Failure<string>("Database connection failed", 
    errorCode: "DB_001", 
    logLevel: LogLevel.Critical);

// Use logging extensions
result.LogIfFailed(logger, "Failed to process user request");

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:

public class SampleUseCase : IUseCaseParameter<string>
{
    public string Name { get; }

    public SampleUseCase(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }
}

Use Case Implementation:

public class SampleUseCaseHandler : IUseCase<SampleUseCase, string>
{
    public async Task<ExecutionResult<string>> ExecuteAsync(SampleUseCase useCaseParameter, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(useCaseParameter.Name))
        {
            return Execution.Failure<string>("Name cannot be empty or whitespace");
        }

        var greeting = $"Hello, {useCaseParameter.Name}! Welcome to FunctionalUseCases.";
        return Execution.Success(greeting);
    }
}

Usage:

var dispatcher = serviceProvider.GetRequiredService<IUseCaseDispatcher>();
var useCaseParameter = new SampleUseCase("World");
var result = await dispatcher.ExecuteAsync(useCaseParameter);

if (result.ExecutionSucceeded)
    Console.WriteLine(result.CheckedValue); // "Hello, World! Welcome to FunctionalUseCases."
else
    Console.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 sample
cd 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

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. Execution Behaviors: Use behaviors for cross-cutting concerns rather than cluttering use case implementations
  10. Behavior Registration: Remember to manually register execution behaviors as they are not automatically discovered

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: Versions are automatically generated based on Git history
  • ๐Ÿ“ฆ NuGet Package Versioning: Packages are versioned consistently across builds
  • ๐Ÿ” Runtime Version Access: Version information is available at runtime via assembly attributes
  • ๐Ÿš€ CI/CD Ready: Integrates seamlessly with build pipelines

Version Information Access

// Access version information at runtime
var assembly = typeof(Execution).Assembly;
var version = assembly.GetName().Version;
var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;

// Example output: "1.0.1+136a4d399f" (includes Git commit hash)
Console.WriteLine($"Library Version: {informationalVersion}");

Dependencies

  • .NET 8.0 or later
  • Microsoft.Extensions.DependencyInjection (8.0.1)
  • Microsoft.Extensions.Logging.Abstractions (8.0.1) - 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.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4-ga4ddd48266 144 8/7/2025