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
<PackageReference Include="Navodkin.PediatR.Core" Version="1.0.0" />
<PackageVersion Include="Navodkin.PediatR.Core" Version="1.0.0" />
<PackageReference Include="Navodkin.PediatR.Core" />
paket add Navodkin.PediatR.Core --version 1.0.0
#r "nuget: Navodkin.PediatR.Core, 1.0.0"
#:package Navodkin.PediatR.Core@1.0.0
#addin nuget:?package=Navodkin.PediatR.Core&version=1.0.0
#tool nuget:?package=Navodkin.PediatR.Core&version=1.0.0
Navodkin.Dotnet.PediatR.Core
<div align="center">
</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:
Separation of Concerns: Allows you to separate business logic from technical aspects (logging, caching, validation, retry logic)
Code Reusability: Write an attribute for logging once and apply it to any method across your project
Functionality Portability: Cross-cutting functionality "travels" with your business logic when refactoring, copying, or moving code
Code Cleanliness: Business logic remains readable and focused on its primary purpose
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 | Versions 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. |
-
.NETFramework 4.8
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
-
.NETStandard 2.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
-
.NETStandard 2.1
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
-
net6.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
-
net7.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
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 |