Navodkin.PediatR.Core 1.0.0

dotnet add package Navodkin.PediatR.Core --version 1.0.0
                    
NuGet\Install-Package Navodkin.PediatR.Core -Version 1.0.0
                    
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="Navodkin.PediatR.Core" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Navodkin.PediatR.Core" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Navodkin.PediatR.Core" />
                    
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 Navodkin.PediatR.Core --version 1.0.0
                    
#r "nuget: Navodkin.PediatR.Core, 1.0.0"
                    
#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 Navodkin.PediatR.Core@1.0.0
                    
#: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=Navodkin.PediatR.Core&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Navodkin.PediatR.Core&version=1.0.0
                    
Install as a Cake Tool

<div align="center">

Navodkin.Dotnet.PediatR.Core Logo

</div>

Powerful method interception library based on attributes for .NET applications


Overview

Navodkin.Dotnet.PediatR.Core is a flexible and extensible library for .NET that provides method interception using declarative attributes. It offers a clean way to add cross-cutting concerns such as logging, validation, retry logic, and performance monitoring to your methods without cluttering business logic.

Why This Library Exists

The Navodkin.Dotnet.PediatR.Core library solves a fundamental problem in modern software development - implementing Cross-Cutting Concerns in .NET applications in a clean, maintainable way.

Key Problems This Library Solves:

  1. Separation of Concerns: Allows you to separate business logic from technical aspects (logging, caching, validation, retry logic)

  2. Code Reusability: Write an attribute for logging once and apply it to any method across your project

  3. Functionality Portability: Cross-cutting functionality "travels" with your business logic when refactoring, copying, or moving code

  4. Code Cleanliness: Business logic remains readable and focused on its primary purpose

  5. Centralized Management: All aspects of cross-cutting functionality are managed in one place through attributes

Practical Benefits:

  • Logging: Automatic method start/end logging with context
  • Caching: Transparent method result caching
  • Validation: Centralized parameter validation
  • Retry Logic: Automatic retries on failures
  • Performance Monitoring: Method execution time measurement
  • Auditing: Automatic user action logging

The Problem Without This Library:

// WITHOUT library - mixed responsibilities
public async Task<User> GetUserAsync(int userId)
{
    _logger.LogInformation("Getting user {UserId}", userId); // Logging
    var stopwatch = Stopwatch.StartNew(); // Performance monitoring

    try
    {
        var user = await _repository.GetByIdAsync(userId); // Business logic
        _logger.LogInformation("User retrieved in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
        return user;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to get user {UserId}", userId);
        throw;
    }
}

Solution With This Library:

// WITH library - clean business logic
[LogStart("GetUser")]
[PerformanceMetricStart("UserQuery", 1000)]
[LogEnd("GetUser")]
public async Task<User> GetUserAsync(int userId)
{
    return await _repository.GetByIdAsync(userId); // Only business logic!
}

https://www.nuget.org/packages/Navodkin.Dotnet.PediatR.Core


Installation

Install via NuGet:

Install-Package Navodkin.Dotnet.PediatR.Core

Or via .NET CLI:

dotnet add package Navodkin.Dotnet.PediatR.Core

Core Components

CoreInterceptor

The CoreInterceptor is the main component that orchestrates method interception. It:

  • Intercepts method calls using expression trees
  • Executes flow attributes in the correct order (StartFlow → Method → EndFlow)
  • Manages execution context and data flow between attributes
  • Supports retry logic when configured
  • Handles exceptions and provides error context
public class CoreInterceptor : ICoreInterceptor
{
    public async Task<TResult> InvokeAsync<TResult>(Expression<Func<Task<TResult>>> methodCall)
    {
        // Intercepts the method call and executes flow attributes
    }
}

Customizable Handlers

The library provides two customizable handler interfaces:

IExecutionHandler

Controls how methods are executed, including retry logic:

public interface IExecutionHandler
{
    Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> action, FlowContext context);
}
IRetryHandler

Manages retry logic with configurable attempts and delays:

public interface IRetryHandler
{
    Task<TResult> ExecuteWithRetryAsync<TResult>(
        Func<Task<TResult>> action,
        FlowContext context,
        int maxAttempts);
}

Flow Attributes

StartFlowAttribute

Executes before method execution. Use for:

  • Logging method start
  • Parameter validation
  • Setting up retry configuration
  • Performance measurement start
public abstract class StartFlowAttribute : Attribute
{
    public abstract Task<object?> ExecuteAsync(IFlowContext context);
}
EndFlowAttribute

Executes after method completion. Use for:

  • Logging method end
  • Caching results
  • Performance measurement end
  • Result transformation
public abstract class EndFlowAttribute : Attribute
{
    public abstract Task<object?> ExecuteAsync(IFlowContext context, object? result);
}

FlowContext

The FlowContext provides execution information and data storage:

public interface IFlowContext
{
    object?[]? Arguments { get; set; }           // Method arguments
    Dictionary<string, object?> Data { get; set; } // Custom data storage
    MethodInfo? Method { get; set; }             // Method information
    IServiceProvider? ServiceProvider { get; set; } // DI container
    object? Target { get; set; }                 // Target object instance

    void ConfigureRetry(int maxAttempts, int delayMilliseconds);
    Exception? GetLastException();
    void AddCustomData(string key, object? value);
    T? GetCustomData<T>(string key);
}

Quick Start

1. Register the Interceptor

using Navodkin.Dotnet.PediatR.Core.Interceptors;
using Navodkin.Dotnet.PediatR.Core.Interceptors.Contracts;

// In your Program.cs or Startup.cs
builder.Services.AddScoped<ICoreInterceptor, CoreInterceptor>();

2. Create a Service with Attributes

using Navodkin.Dotnet.PediatR.Example.Attributes;

public class UserService
{
    [LogStart("UserOperation")]
    [LogEnd("UserOperation")]
    public async Task<User> GetUserAsync(int userId)
    {
        // Your business logic here
        return await _repository.GetByIdAsync(userId);
    }
}

3. Use the Interceptor

public class UserController : ControllerBase
{
    private readonly ICoreInterceptor _interceptor;
    private readonly UserService _userService;

    public UserController(ICoreInterceptor interceptor, UserService userService)
    {
        _interceptor = interceptor;
        _userService = userService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id, CancellationToken cancellationToken = default)
    {
        var result = await _interceptor.InvokeAsync(() => _userService.GetUserAsync(id), cancellationToken);
        return Ok(result);
    }
}

Cancellation Token Support

The library fully supports cancellation tokens for graceful operation cancellation:

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id, CancellationToken cancellationToken = default)
{
    // The cancellation token is automatically passed to all attributes and handlers
    var result = await _interceptor.InvokeAsync(() => _userService.GetUserAsync(id), cancellationToken);
    return Ok(result);
}

Cancellation Behavior

  • StartFlow attributes: Check for cancellation before execution
  • EndFlow attributes: Check for cancellation before execution
  • Retry logic: Respects cancellation during retry delays
  • Execution handlers: Check for cancellation before method execution

Example with Timeout

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var result = await _interceptor.InvokeAsync(() => _service.ProcessDataAsync(data), cts.Token);
    return Ok(result);
}
catch (OperationCanceledException)
{
    return StatusCode(408, "Request timeout");
}

Built-in Attributes

Logging Attributes

LogStartAttribute

Logs method execution start with custom message:

[LogStart("UserOperation")]
public async Task<User> GetUserAsync(int userId) { ... }
LogEndAttribute

Logs method execution end with duration calculation:

[LogEnd("UserOperation")]
public async Task<User> GetUserAsync(int userId) { ... }

Performance Monitoring

PerformanceMetricStartAttribute

Starts performance measurement:

[PerformanceMetricStart("UserQuery", 1000)] // 1000ms warning threshold
public async Task<User> GetUserAsync(int userId) { ... }
PerformanceMetricEndAttribute

Ends performance measurement and logs results:

[PerformanceMetricEnd("UserQuery")]
public async Task<User> GetUserAsync(int userId) { ... }

Retry Logic

RetryAttribute

Configures retry behavior:

[Retry(maxAttempts: 3, delayMilliseconds: 1000)]
public async Task<User> GetUserAsync(int userId) { ... }

Caching

CacheAttribute

Caches method results:

[Cache(durationSeconds: 300, cacheKey: "user_{0}")]
public async Task<User> GetUserAsync(int userId) { ... }

Validation

ValidateAttribute

Validates method parameters:

[Validate<GetUserPaymentsByIdValidator>]
public async Task<List<Payment>> GetUserPaymentsAsync(GetUserPaymentsByIdQuery query) { ... }

Custom Implementation Examples

Custom StartFlow Attribute

public class SecurityCheckAttribute : StartFlowAttribute
{
    private readonly string _requiredRole;

    public SecurityCheckAttribute(string requiredRole)
    {
        _requiredRole = requiredRole;
    }

    public override async Task<object?> ExecuteAsync(IFlowContext context)
    {
        var userService = context.ServiceProvider?.GetService<IUserService>();
        var currentUser = await userService?.GetCurrentUserAsync();

        if (currentUser?.Role != _requiredRole)
        {
            throw new UnauthorizedAccessException($"Role {_requiredRole} required");
        }

        context.AddCustomData("User", currentUser);
        return null;
    }
}

Custom EndFlow Attribute

public class AuditLogAttribute : EndFlowAttribute
{
    private readonly string _action;

    public AuditLogAttribute(string action)
    {
        _action = action;
    }

    public override async Task<object?> ExecuteAsync(IFlowContext context, object? result)
    {
        var auditService = context.ServiceProvider?.GetService<IAuditService>();
        var user = context.GetCustomData<User>("User");

        await auditService?.LogAsync(new AuditEntry
        {
            Action = _action,
            UserId = user?.Id,
            MethodName = context.Method?.Name,
            Timestamp = DateTime.UtcNow,
            Success = result != null
        });

        return result;
    }
}

Custom Execution Handler

public class CircuitBreakerExecutionHandler : IExecutionHandler
{
    private readonly IExecutionHandler _innerHandler;
    private readonly CircuitBreaker _circuitBreaker;

    public CircuitBreakerExecutionHandler(IExecutionHandler innerHandler, CircuitBreaker circuitBreaker)
    {
        _innerHandler = innerHandler;
        _circuitBreaker = circuitBreaker;
    }

    public async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> action, FlowContext context)
    {
        return await _circuitBreaker.ExecuteAsync(() => _innerHandler.ExecuteAsync(action, context));
    }
}

Custom Retry Handler

public class ExponentialBackoffRetryHandler : IRetryHandler
{
    private readonly ILogger<ExponentialBackoffRetryHandler> _logger;

    public async Task<TResult> ExecuteWithRetryAsync<TResult>(
        Func<Task<TResult>> action,
        FlowContext context,
        int maxAttempts)
    {
        var baseDelay = context.GetCustomData<int>("BaseDelay") ?? 1000;

        for (var attempt = 1; attempt <= maxAttempts; attempt++)
        {
            try
            {
                return await action();
            }
            catch (Exception ex)
            {
                if (attempt == maxAttempts) throw;

                var delay = baseDelay * Math.Pow(2, attempt - 1);
                _logger.LogWarning("Attempt {Attempt} failed, retrying in {Delay}ms", attempt, delay);
                await Task.Delay((int)delay);
            }
        }

        throw new InvalidOperationException("Retry failed");
    }
}

Real-World Example

Here's a complete example from the sample project:

[ApiController]
[Route("api/[controller]")]
public class UserPaymentController : ControllerBase
{
    private readonly UserPaymentInteractor _interactor;
    private readonly ICoreInterceptor _interceptor;

    [HttpGet("user/{userId}")]
    public async Task<ActionResult<List<UserPaymentTableItem>>> GetUserPayments(int userId, string userName)
    {
        var query = new GetUserPaymentsByIdQuery(userId, userName);

        // The interceptor will execute all attributes in the correct order
        var result = await _interceptor.InvokeAsync(() => _interactor.GetUserPaymentsByIdAsync(query));

        return Ok(result);
    }
}

// The interactor with attributes
public class UserPaymentInteractor
{
    [LogStart("GetUserPayments")]
    [Validate<GetUserPaymentsByIdValidator>]
    [Retry(maxAttempts: 3, delayMilliseconds: 1000)]
    [PerformanceMetricStart("UserPaymentsQuery", 2000)]
    [Cache(durationSeconds: 300)]
    [PerformanceMetricEnd("UserPaymentsQuery")]
    [LogEnd("GetUserPayments")]
    public async Task<List<UserPaymentTableItem>> GetUserPaymentsByIdAsync(GetUserPaymentsByIdQuery query)
    {
        // Business logic here
        return await _repository.GetUserPaymentsAsync(query);
    }
}

Advanced Configuration

Custom Service Registration

// Register with custom handlers
builder.Services.AddScoped<ICoreInterceptor>(provider =>
{
    var serviceProvider = provider;
    var customRetryHandler = new ExponentialBackoffRetryHandler(
        provider.GetService<ILogger<ExponentialBackoffRetryHandler>>());
    var customExecutionHandler = new CircuitBreakerExecutionHandler(
        new DefaultExecutionHandler(customRetryHandler,
            provider.GetService<ILogger<DefaultExecutionHandler>>()),
        new CircuitBreaker());

    return new CoreInterceptor(serviceProvider, customRetryHandler, customExecutionHandler);
});

Flow Context Customization

public class CustomFlowContext : FlowContext
{
    public string? CorrelationId { get; set; }
    public string? UserId { get; set; }

    public void SetCorrelationId(string correlationId)
    {
        CorrelationId = correlationId;
        AddCustomData("CorrelationId", correlationId);
    }
}

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 is compatible.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.0 212 10/13/2025