EricksonLopez.Mediator.OpenTelemetry 2.0.0

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

EricksonLopez.Mediator

Ultra-high-performance, zero-allocation, compile-time monomorphized CQRS mediator and pipeline ecosystem for modern .NET.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT


EricksonLopez.Mediator is an enterprise-grade, high-throughput in-process messaging infrastructure engineered specifically for modern .NET (8.0, 9.0, and 10.0+). It eliminates all runtime reflection, dynamic delegate allocations, and runtime assembly scanning by moving handler routing, dependency injection registration, and pipeline weaving entirely to compile time via Roslyn Incremental Source Generators. Leveraging unboxed struct INext<TResponse> continuations and strict CQRS type segregation (ICommand<T> vs IQuery<T>), it delivers sub-2-nanosecond dispatch latency, 0 bytes of heap allocation on the synchronous completion hot path, and 100% Native AOT compatibility.


Table of Contents


๐ŸŽฏ What Problem It Solves

Traditional mediator implementations (such as MediatR v12 or reflection-based frameworks) introduce critical architectural and runtime limitations in modern cloud-native .NET systems:

  1. The Hidden Cost of Runtime Reflection and Delegate Allocations: Traditional mediators construct dynamic delegate chains (RequestHandlerDelegate<T>), wrap requests in heap-allocated closure objects, and resolve handlers via dynamic MakeGenericMethod or reflection scanning. Every dispatched request creates GC pressure (48 B to 112+ B allocated per invocation), inducing GC pauses under heavy load.
  2. Native AOT Trimming Failures: Dynamic assembly scanning (services.AddMediatR(typeof(Program))) and unbound reflection fail under .NET Native AOT compilation, causing runtime exceptions (MissingMethodException, TypeInitializationException) unless extensive trim-descriptor configuration is maintained.
  3. Primitive CQRS Obsession: Conflating read and write operations under a single untyped IRequest<TResponse> interface prevents architectural enforcement. Developers inadvertently introduce side effects into read queries or query semantics into mutation commands without compiler oversight.
  4. Silent Architecture Drift & Late Runtime Failures: Missing handlers, duplicate handler registrations, or misordered pipeline behaviors are only discovered at runtime when an endpoint is hit or during integration testing.

How EricksonLopez.Mediator Solves This

  • 0 Bytes Allocated on the Synchronous Completion Hot Path: Struct-based continuations (struct INext<TResponse>) allow the compiler to inline pipeline steps directly into a monomorphic execution chain, eliminating delegate boxing and heap closures.
  • Compile-Time Switch Monomorphization: Handlers and pipeline behaviors are discovered and stitched at build time by the Roslyn Incremental Source Generator into static C# pattern matches.
  • Strict CQRS Segregation: Dedicated ICommand<TResponse>, IQuery<TResponse>, INotification, and IStreamRequest<TResponse> interfaces enforce architectural intent in the C# type system.
  • Instant IDE Diagnostics (ELM001โ€“ELM011): Catch missing handlers, duplicate CQRS handlers, signature errors, and pipeline ordering conflicts directly in the editor as compile errors before code runs.
  • 100% Native AOT & Trimming Compliant: Zero reflection in the Core hot path guarantees flawless compilation and execution on bare metal.

โšก Key Features

  • โšก Sub-2ns Dispatch Latency: Direct monomorphized type-switch dispatching executes faster than runtime reflection delegates.
  • ๐Ÿง  Zero-Allocation Pipeline Continuations: Custom pipeline behaviors implement where TNext : struct, INext<TResponse>, eliminating delegate allocations.
  • ๐Ÿ›ก๏ธ Strict CQRS in Type System: Segregated ICommand<T> and IQuery<T> contracts prevent architectural antipatterns.
  • ๐Ÿ” Compile-Time Roslyn Diagnostics: 11 dedicated analyzer rules (ELM001โ€“ELM011) prevent misconfigurations at build time.
  • ๐Ÿ“ฆ First-Party Integrated Ecosystem: Official zero-overhead packages for OpenTelemetry, Polly v8, FluentValidation, RateLimiting, and Minimal APIs.
  • ๐Ÿ”” Flexible Domain Event Publishing: Support for Sequential (default), Parallel (Task.WhenAll), and SequentialAggregateExceptions dispatch strategies via [PublishStrategy].
  • ๐ŸŒŠ Reactive Asynchronous Streaming: First-class support for IStreamRequest<T> returning IAsyncEnumerable<T> with zero pipeline overhead.
  • ๐Ÿงช Production-Ready Test Doubles: Official FakeMediator and DelegateNext<T> eliminate mocking boilerplate in unit test suites.
  • โ˜๏ธ Zero-DI Serverless Ready: StaticMediator allows direct handler dispatching in AWS Lambda, Azure Functions, or high-performance CLI tools without DI container overhead. Use SendCommand<TCommand, TResponse>() and SendQuery<TQuery, TResponse>() for AOT-safe static dispatch.

๐Ÿ“ฆ Ecosystem

Package Version Description
EricksonLopez.Mediator NuGet Core interfaces (ISender, IPublisher, IMediator, ICommand, IQuery, INotification, IStreamRequest), struct continuations, and StaticMediator.
EricksonLopez.Mediator.Generator NuGet Roslyn Incremental Source Generator and Analyzer for compile-time monomorphized dispatch and diagnostics (ELM001โ€“ELM011).
EricksonLopez.Mediator.AspNetCore NuGet Minimal API endpoint routing extensions (MapCommand, MapQuery) connecting routes directly to mediator handlers.
EricksonLopez.Mediator.Caching NuGet High-performance query caching and invalidation pipeline behaviors with multi-tier caching and 100% Native AOT compatibility.
EricksonLopez.Mediator.FluentValidation NuGet High-performance FluentValidation pipeline integration via ValidationPipelineBehavior<T,R> and AddMediatorFluentValidation().
EricksonLopez.Mediator.OpenTelemetry NuGet Zero-overhead distributed tracing (ActivitySource) and performance metrics (Meter) with pre-cached metadata.
EricksonLopez.Mediator.Polly NuGet โš ๏ธ DEPRECATED (ADR-036) โ€” Direct Polly v8 integration; migrate to EricksonLopez.Resilience.Mediator.
EricksonLopez.Mediator.RateLimiting NuGet High-throughput rate limiting pipeline behavior built on System.Threading.RateLimiting.
EricksonLopez.Mediator.Result NuGet Result pattern abstraction (IResultFactory<TResponse>) bridging pipeline short-circuiting with EricksonLopez.Result.
EricksonLopez.Mediator.Testing NuGet Official in-memory FakeMediator and DelegateNext test doubles for isolated unit testing.

๐Ÿ“š Documentation

๐ŸŒ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-mediator/tree/main/docs

๐ŸŽ“ Step-by-Step Interactive Showcase (Levels 00 to 13)

Level Topic Description
Level 00 Introduction & Philosophy Core architectural foundations, CQRS segregation, and zero-allocation vision.
Level 01 Getting Started Basic command/query definitions, handler implementation, and DI registration.
Level 02 Configuration & Service Lifetimes Configurable handler lifetimes (Singleton, Scoped, Transient) and multi-assembly discovery.
Level 03 Strict CQRS Mechanics Segregated ICommand<T> vs IQuery<T> contracts and single-handler compiler invariants.
Level 04 Events & Notifications Domain event publishing, multi-subscriber dispatch, and custom publish strategies.
Level 05 Pipelines & Struct Behaviors Zero-allocation cross-cutting middleware using struct INext<TResponse>.
Level 06 Error Handling & Result Pattern Exception-free short-circuiting with IResultFactory<TResponse> and EricksonLopez.Result.
Level 07 Performance & Native AOT Compile-time monomorphization, RyuJIT optimization, and trimming validation.
Level 08 Customization & Extension Points Custom pipeline behaviors, notification behaviors, and serverless StaticMediator.
Level 09 Official Ecosystem Extensions Deep dive into OpenTelemetry, Polly v8, FluentValidation, RateLimiting, and Minimal APIs.
Level 10 Enterprise Patterns & Domain Events Unit of work coordination, aggregate exception handling, and enterprise CQRS workflows.
Level 11 Dependency Injection Architecture Monomorphized AddEricksonLopezMediator() DI mechanics and lifetime scope boundaries.
Level 12 Testing & Test Doubles Writing fast, reflection-free unit and integration tests using FakeMediator and DelegateNext.
Level 13 Diagnostics, Tracing & Metrics Resolving Roslyn rules ELM001โ€“ELM011 and consuming OpenTelemetry activity sources.

๐Ÿ–ฅ๏ธ Executable C# Showcase (Levels 0 to 11)

The official living reference implementation โ€” fully compilable, runnable C# source demonstrating 100% of the public API:

dotnet run --project samples/EricksonLopez.Mediator.Samples/EricksonLopez.Mediator.Samples.csproj
Level File Topic
Level 0 Level0_Conceptual.cs Introduction & Philosophy
Level 1 Level1_QuickStart.cs Quick Start & First Handler
Level 2 Level2_FullConfig.cs Full Configuration & Global Behaviors
Level 3 Level3_RealUseCases.cs Real Use Cases โ€” CQRS + Events
Level 4 Level4_AdvancedIntegration.cs Advanced Integration โ€” Result Pattern + MediatR Compat
Level 5 Level5_Processing.cs Processing โ€” Parallel Events & Streaming
Level 6 Level6_ErrorHandling.cs Error Handling โ€” Aggregation & Rate Limiting
Level 7 Level7_Scalability.cs Scalability โ€” Throughput & Multi-handler
Level 8 Level8_Customization.cs Customization โ€” Lifetimes & Discovery
Level 9 Level9_Extensions.cs Extensions โ€” OTel, Caching, FluentValidation, Polly
Level 10 Level10_EnterpriseArchitecture.cs Enterprise Architecture & StaticMediator
Level 11 Level11_Testing.cs Testing โ€” FakeMediator, DelegateNext & INotificationBehavior

๐Ÿ“– Technical Reference & Architecture Guides


๐Ÿ“ฅ Installation

1. Core Package & Roslyn Source Generator (Required)

Install the core abstractions and the Roslyn source generator analyzer:

# Install Core abstractions
dotnet add package EricksonLopez.Mediator

# Install Roslyn Source Generator (as a build analyzer)
dotnet add package EricksonLopez.Mediator.Generator --output-item-type Analyzer

Or configure directly in your .csproj:

<ItemGroup>
  <PackageReference Include="EricksonLopez.Mediator" Version="2.0.0" />
  <PackageReference Include="EricksonLopez.Mediator.Generator" Version="2.0.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

2. Optional Framework & Integration Packages

# Minimal APIs Integration for ASP.NET Core
dotnet add package EricksonLopez.Mediator.AspNetCore

# In-Memory & Distributed Caching Integration
dotnet add package EricksonLopez.Mediator.Caching

# OpenTelemetry Tracing and Metrics
dotnet add package EricksonLopez.Mediator.OpenTelemetry

# Polly v8 Resilience Integration
dotnet add package EricksonLopez.Mediator.Polly

# High-Throughput Rate Limiting
dotnet add package EricksonLopez.Mediator.RateLimiting

# Result Pattern Short-Circuiting Integration
dotnet add package EricksonLopez.Mediator.Result

# Recommended FluentValidation Integration
dotnet add package EricksonLopez.Mediator.FluentValidation

3. Unit Testing & Assertions Package

# Test Doubles (FakeMediator, DelegateNext) for Unit Tests
dotnet add package EricksonLopez.Mediator.Testing

๐Ÿš€ Quick Start

Step 1: Define Strongly-Typed Commands, Queries, and Notifications

Segregate your business operations cleanly using ICommand<TResponse>, IQuery<TResponse>, and INotification:

using System;
using EricksonLopez.Mediator;

// 1. Command: State mutation intent
public sealed record CreateOrderCommand(string CustomerId, decimal Amount) : ICommand<Guid>;

// 2. Query: Read-only data request
public sealed record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto?>;
public sealed record OrderDto(Guid OrderId, string CustomerId, decimal Amount, string Status);

// 3. Notification: Domain event published to multiple subscribers
public sealed record OrderCreatedEvent(Guid OrderId, string CustomerId) : INotification;

Step 2: Implement Handlers with Zero-Allocation ValueTask<T>

using System;
using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Mediator;

// Command Handler
public sealed class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    private readonly IPublisher _publisher;

    public CreateOrderCommandHandler(IPublisher publisher) => _publisher = publisher;

    public async ValueTask<Guid> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
    {
        var orderId = Guid.NewGuid();
        
        // Publish domain event
        await _publisher.Publish(new OrderCreatedEvent(orderId, command.CustomerId), cancellationToken);
        
        return orderId;
    }
}

// Query Handler
public sealed class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderDto?>
{
    public ValueTask<OrderDto?> Handle(GetOrderByIdQuery query, CancellationToken cancellationToken)
    {
        // Return cached or fetched DTO without Task allocation
        var dto = new OrderDto(query.OrderId, "CUST-42", 150.00m, "Confirmed");
        return ValueTask.FromResult<OrderDto?>(dto);
    }
}

// Notification Handler (Subscriber)
public sealed class OrderCreatedAuditHandler : INotificationHandler<OrderCreatedEvent>
{
    public ValueTask Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[Audit] Order {notification.OrderId} created for customer {notification.CustomerId}");
        return ValueTask.CompletedTask;
    }
}

Step 3: Register Dependencies at Compile-Time

Register all handlers, behaviors, and generated dispatchers with a single call:

using Microsoft.Extensions.DependencyInjection;
using EricksonLopez.Mediator;

var services = new ServiceCollection();

// Automatically registers all handlers, behaviors, and generated monomorphic dispatchers
// Registers IMediator, ISender, and IPublisher as Scoped by default (ADR-037)
// Pass ServiceLifetime.Singleton for stateless background workers: services.AddEricksonLopezMediator(ServiceLifetime.Singleton);
services.AddEricksonLopezMediator();

var serviceProvider = services.BuildServiceProvider();

Step 4: Dispatch via IMediator, ISender, or IPublisher

var mediator = serviceProvider.GetRequiredService<IMediator>();

// Dispatch Command (Writes)
Guid orderId = await mediator.Send(new CreateOrderCommand("CUST-42", 150.00m));

// Dispatch Query (Reads)
OrderDto? order = await mediator.Send(new GetOrderByIdQuery(orderId));

// Publish Notification (Events)
await mediator.Publish(new OrderCreatedEvent(orderId, "CUST-42"));

Step 5: Asynchronous Reactive Streaming with IStreamRequest<T>

public sealed record StreamPricesRequest(string Symbol) : IStreamRequest<decimal>;

public sealed class StreamPricesRequestHandler : IStreamRequestHandler<StreamPricesRequest, decimal>
{
    public async IAsyncEnumerable<decimal> Handle(
        StreamPricesRequest request, 
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(50, cancellationToken);
            yield return 100.0m + i;
        }
    }
}

// Consuming the reactive stream
await foreach (var price in mediator.CreateStream(new StreamPricesRequest("MSFT")))
{
    Console.WriteLine($"Live Price: {price:C}");
}

๐Ÿ’ก Core Use Cases

Use Case 1: Clean Architecture CQRS Command Handlers

Enforce strict boundaries between application command handling, state persistence, and domain event dispatching:

public sealed record RegisterUserCommand(string Username, string Email) : ICommand<Guid>;

public sealed class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand, Guid>
{
    private readonly IUserRepository _repository;
    private readonly IPublisher _publisher;

    public RegisterUserCommandHandler(IUserRepository repository, IPublisher publisher)
    {
        _repository = repository;
        _publisher = publisher;
    }

    public async ValueTask<Guid> Handle(RegisterUserCommand command, CancellationToken cancellationToken)
    {
        var user = new User(Guid.NewGuid(), command.Username, command.Email);
        await _repository.SaveAsync(user, cancellationToken);
        await _publisher.Publish(new UserRegisteredEvent(user.Id, user.Email), cancellationToken);
        return user.Id;
    }
}

Use Case 2: Zero-Allocation Cached Query Handlers

Avoid Task allocations on cache hits by leveraging ValueTask<TResponse>:

public sealed record GetProductByIdQuery(Guid ProductId) : IQuery<ProductDto?>;

public sealed class GetProductByIdQueryHandler : IQueryHandler<GetProductByIdQuery, ProductDto?>
{
    private readonly IMemoryCache _cache;
    private readonly IDbConnection _db;

    public GetProductByIdQueryHandler(IMemoryCache cache, IDbConnection db)
    {
        _cache = cache;
        _db = db;
    }

    public ValueTask<ProductDto?> Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
    {
        if (_cache.TryGetValue(query.ProductId, out ProductDto? cached))
        {
            // Zero heap allocation on synchronous cache hit
            return ValueTask.FromResult(cached);
        }

        return FetchFromDatabaseAsync(query.ProductId, cancellationToken);
    }

    private async ValueTask<ProductDto?> FetchFromDatabaseAsync(Guid id, CancellationToken ct)
    {
        var product = await _db.QuerySingleOrDefaultAsync<ProductDto>(id, ct);
        if (product is not null)
            _cache.Set(id, product, TimeSpan.FromMinutes(10));
        return product;
    }
}

Use Case 3: Zero-Allocation Cross-Cutting Pipeline Behaviors

Intercept requests with zero delegate overhead using unboxed struct INext<TResponse> continuations:

public sealed class PerformanceLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<PerformanceLoggingBehavior<TRequest, TResponse>> _logger;

    public PerformanceLoggingBehavior(ILogger<PerformanceLoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async ValueTask<TResponse> Handle<TNext>(
        TRequest request, 
        TNext next, 
        CancellationToken cancellationToken)
        where TNext : struct, INext<TResponse> // Required constraint for zero-allocation
    {
        var start = Stopwatch.GetTimestamp();
        var response = await next.InvokeAsync().ConfigureAwait(false);
        var elapsedMs = Stopwatch.GetElapsedTime(start).TotalMilliseconds;

        if (elapsedMs > 500)
        {
            _logger.LogWarning("Long running request: {Request} took {ElapsedMs}ms", typeof(TRequest).Name, elapsedMs);
        }

        return response;
    }
}

Use Case 4: Exception-Free Pipeline Short-Circuiting with Result Pattern

Integrate with EricksonLopez.Result via IResultFactory<TResponse> to short-circuit validation failures without throwing expensive exceptions:

using EricksonLopez.Mediator.Result;
using EricksonLopez.Result;

public sealed class ValidationPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly IResultFactory<TResponse>? _resultFactory;

    public ValidationPipelineBehavior(IResultFactory<TResponse>? resultFactory = null)
    {
        _resultFactory = resultFactory;
    }

    public ValueTask<TResponse> Handle<TNext>(TRequest request, TNext next, CancellationToken cancellationToken)
        where TNext : struct, INext<TResponse>
    {
        if (request is CreateOrderCommand cmd && cmd.Amount <= 0)
        {
            if (_resultFactory is not null)
            {
                var error = Error.Validation("Order.InvalidAmount", "Order amount must be greater than zero.");
                // Returns Result<T>.Failure without exception overhead or stack unwinding
                return new ValueTask<TResponse>(_resultFactory.CreateFailure(error));
            }
        }

        return next.InvokeAsync();
    }
}

Use Case 5: Resilient Domain Event Fan-Out with Exception Aggregation

Publish critical domain notifications where all subscribers must execute even if preceding handlers fail:

[PublishStrategy(PublishStrategy.SequentialAggregateExceptions)]
public sealed record PaymentCompletedEvent(Guid PaymentId, decimal Amount) : INotification;

// Invocation site with structured exception aggregation handling
try
{
    await mediator.Publish(new PaymentCompletedEvent(paymentId, amount), cancellationToken);
}
catch (NotificationHandlerAggregateException aggEx)
{
    foreach (var inner in aggEx.HandlerExceptions)
    {
        logger.LogError(inner, "Subscriber failed during PaymentCompletedEvent dispatch.");
    }
}

Use Case 6: Reactive Database Streaming with Backpressure

Stream large datasets directly to HTTP consumers without buffering entire collections into memory:

public sealed record ExportAuditLogsRequest(DateTime FromUtc) : IStreamRequest<AuditRecordDto>;

public sealed class ExportAuditLogsRequestHandler : IStreamRequestHandler<ExportAuditLogsRequest, AuditRecordDto>
{
    private readonly IDbContext _dbContext;

    public ExportAuditLogsRequestHandler(IDbContext dbContext) => _dbContext = dbContext;

    public async IAsyncEnumerable<AuditRecordDto> Handle(
        ExportAuditLogsRequest request, 
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var row in _dbContext.StreamAuditLogsAsync(request.FromUtc, cancellationToken))
        {
            yield return new AuditRecordDto(row.Id, row.TimestampUtc, row.Action);
        }
    }
}

๐Ÿ”Œ Configuration & Integrations

ASP.NET Core Minimal APIs

Eliminate repetitive controller boilerplate by mapping CQRS commands and queries directly to ASP.NET Core Minimal API route endpoints using EricksonLopez.Mediator.AspNetCore:

using EricksonLopez.Mediator;
using EricksonLopez.Mediator.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEricksonLopezMediator();

var app = builder.Build();

// Expose CQRS handlers directly on HTTP routes
app.MapCommand<CreateOrderCommand, Guid>("/api/orders");
app.MapQuery<GetOrderByIdQuery, OrderDto?>("/api/orders/{orderId:guid}");

app.Run();

OpenTelemetry Distributed Tracing & Metrics

Export standard OpenTelemetry ActivitySource traces and Meter instruments with pre-cached metadata and zero runtime reflection via EricksonLopez.Mediator.OpenTelemetry:

using EricksonLopez.Mediator.OpenTelemetry;

builder.Services.AddMediatorOpenTelemetry(options =>
{
    options.ActivitySourceName = "Enterprise.Mediator";
    options.EnrichActivity = (activity, request) =>
    {
        activity.SetTag("messaging.system", "ericksonlopez_mediator");
        activity.SetTag("messaging.destination", request.GetType().Name);
    };
});

Polly v8 Resilience Policies

MIGRATION NOTICE (ADR-036): EricksonLopez.Mediator.Polly is maintained for existing Polly v8 integration. For new projects and Clean Architecture compliance without leaking Polly types into the Application layer, migrate to EricksonLopez.Resilience.Mediator from the EricksonLopez.Resilience ecosystem. See ADR-036 for the full migration guide.

The following example shows legacy usage for teams that have not yet migrated:

using EricksonLopez.Mediator.Polly;
using Polly;

// Register resilience strategies in Program.cs
builder.Services.AddMediatorDefaultResiliencePipeline(pipelineBuilder =>
{
    pipelineBuilder.AddRetry(new Polly.Retry.RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(100),
        BackoffType = DelayBackoffType.Exponential
    });
    pipelineBuilder.AddTimeout(TimeSpan.FromSeconds(5));
});

// Decorate command with resilience strategy
[UseResiliencePipeline("Default")]
public sealed record SyncExternalInventoryCommand(string Sku) : ICommand<bool>;

FluentValidation Pipeline

Integrate FluentValidation rules automatically using EricksonLopez.Mediator.FluentValidation:

using EricksonLopez.Mediator.FluentValidation;
using FluentValidation;

public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty().MaximumLength(50);
        RuleFor(x => x.Amount).GreaterThan(0);
    }
}

// Program.cs registration
builder.Services.AddMediatorFluentValidation();
builder.Services.AddMediatorFluentValidationValidator<CreateOrderCommandValidator, CreateOrderCommand>();

System.Threading.RateLimiting

Protect handler execution from resource starvation with in-process rate limiting via EricksonLopez.Mediator.RateLimiting:

using System.Threading.RateLimiting;
using EricksonLopez.Mediator.RateLimiting;

builder.Services.AddSingleton<RateLimiter>(_ => new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
    TokenLimit = 200,
    TokensPerPeriod = 100,
    ReplenishmentPeriod = TimeSpan.FromSeconds(1),
    QueueLimit = 20
}));

builder.Services.AddMediatorRateLimiting();

Roslyn Diagnostic Analyzers

EricksonLopez.Mediator.Generator inspects your code at compile time and emits instant compiler diagnostics to prevent architectural bugs:

Diagnostic ID Severity Category Description CodeFix / Remediation
ELM001 Error Architecture No handler found for request type (ICommand or IQuery). Implement missing ICommandHandler or IQueryHandler, or add [DiscoverHandlers].
ELM002 Error CQRS Invariant Duplicate command handler detected for the same ICommand<T>. Remove or consolidate duplicate handlers; exactly one handler is permitted per command.
ELM003 Error CQRS Invariant Duplicate query handler detected for the same IQuery<T>. Remove or consolidate duplicate handlers; exactly one handler is permitted per query.
ELM004 Error Type Safety Invalid handler method signature or return type. Ensure Handle method returns ValueTask<TResponse> and takes (TRequest, CancellationToken).
ELM005 Warning AOT / Trimming Open generic handler cannot be statically resolved at compile time. Define closed-generic handler implementations or explicit type registrations.
ELM006 Warning Architecture Notification has no registered handlers (dead event warning). Implement INotificationHandler<T> if the published event requires subscribers.
ELM007 Error Pipeline Safety Open generic behavior has invalid generic constraints or arity. Ensure behaviors implement IPipelineBehavior<TRequest, TResponse> with valid constraints.
ELM008 Warning Pipeline Safety Behavior ordering conflict (duplicate explicit order index). Assign unique sequential integer indices to [UseBehavior(..., order: N)].
ELM009 Error Streaming No stream handler found for IStreamRequest<T>. Implement missing IStreamRequestHandler<TRequest, TResponse>.
ELM010 Error Streaming Multiple stream handlers found for the same IStreamRequest<T>. Ensure only one stream handler exists per stream request type.
ELM011 Error Type Safety Invalid stream handler method signature. Ensure Handle method returns IAsyncEnumerable<TResponse>.

Compile-Time Validation Attributes

The core EricksonLopez.Mediator package includes built-in declarative validation attributes processed by the source generator at compile time. When [ValidateRequest] is applied to a request type, the generator emits a validation guard in the dispatch path โ€” no runtime reflection, no external dependencies, 100% AOT-safe.

[ValidateRequest]
public sealed record CreateOrderCommand(
    [ValidateNotEmpty] string CustomerId,
    [ValidateRange(1.0, 1_000_000.0)] decimal Amount,
    [ValidateNotNull, ValidateLength(1, 200)] string? Notes
) : ICommand<Guid>;
Attribute Target Description
[ValidateRequest] Class / Struct Enables compile-time validation code generation for the decorated request type
[ValidateNotNull] Property / Field / Parameter Validates the value is not null
[ValidateNotEmpty] Property / Field / Parameter Validates the string is not null or whitespace
[ValidateLength(min, max)] Property / Field / Parameter Validates string length is within [min, max]
[ValidateRange(min, max)] Property / Field / Parameter Validates a numeric value is within [min, max]
[ValidateRegex(pattern)] Property / Field / Parameter Validates the string matches the specified regular expression

Validation failures throw MediatorValidationException containing an IReadOnlyList<string> Errors collection with all failing constraint messages.

These attributes provide lightweight, compile-time-generated validation in the core package. For rich rule-based validation using validators, FluentValidation expressions, and custom validators, use EricksonLopez.Mediator.FluentValidation instead.

Built-In Health Checks

EricksonLopez.Mediator ships with a built-in mediator readiness health check that integrates with ASP.NET Core's IHealthChecksBuilder:

// Register in Program.cs
builder.Services.AddHealthChecks()
    .AddMediatorHealthCheck();

// Expose the health endpoint
app.MapHealthChecks("/health");

MediatorHealthCheck verifies that the mediator can resolve its core dispatch infrastructure and reports Healthy or Unhealthy accordingly. The check is registered with the "mediator" tag, allowing health endpoints to filter by tag:

app.MapHealthChecks("/health/mediator", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("mediator")
});

๐Ÿงช Testing & Quality

Unit Testing with FakeMediator

Eliminate dynamic proxy mocking overhead (Moq/NSubstitute) by using the official FakeMediator test double:

using System;
using System.Threading.Tasks;
using EricksonLopez.Mediator.Testing;
using Xunit;

public sealed class OrderServiceTests
{
    [Fact]
    public async Task PlaceOrder_WhenValid_DispatchesCreateOrderCommand()
    {
        // Arrange
        var fakeMediator = new FakeMediator();
        var expectedOrderId = Guid.NewGuid();
        fakeMediator.SetupCommand<CreateOrderCommand, Guid>(cmd => expectedOrderId);

        var service = new OrderService(fakeMediator);

        // Act
        var resultId = await service.PlaceOrderAsync("CUST-99", 250.00m);

        // Assert: Fluent assertions API
        Assert.Equal(expectedOrderId, resultId);
        fakeMediator.ShouldHaveReceived<CreateOrderCommand>(c => c.CustomerId == "CUST-99" && c.Amount == 250.00m);
        Assert.Equal(1, fakeMediator.ReceivedCount<CreateOrderCommand>());
    }
}

Isolated Behavior Testing with DelegateNext

Test individual IPipelineBehavior<TRequest, TResponse> implementations in complete isolation without instantiating DI pipelines:

using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Mediator.Testing;
using Xunit;

public sealed class LoggingBehaviorTests
{
    [Fact]
    public async Task LoggingBehavior_InvokesNextStepSuccessfully()
    {
        // Arrange
        var behavior = new LoggingBehavior<CreateOrderCommand, Guid>(NullLogger<LoggingBehavior<CreateOrderCommand, Guid>>.Instance);
        var expectedId = Guid.NewGuid();
        var nextStub = new DelegateNext<Guid>(expectedId); // Constant result stub continuation

        // Act
        var result = await behavior.Handle(
            new CreateOrderCommand("CUST-1", 100m), 
            nextStub, 
            CancellationToken.None);

        // Assert
        Assert.Equal(expectedId, result);
    }
}

Mutation Testing & Quality Gates

The codebase enforces strict DevSecOps quality gates verified via GitHub Actions:

  • 100% Test Pass Rate: Verified across .NET 8.0 LTS, .NET 9.0 STS, and .NET 10.0 LTS.
  • Native AOT Smoke Testing: Automated compilation and test execution under PublishAot=true with zero trim warnings (TreatWarningsAsErrors=true).
  • Public API Analyzers: Public surface changes guarded by Microsoft.CodeAnalysis.PublicApiAnalyzers (RS0016/RS0017).
  • Multi-Framework Runner Support: Test doubles work seamlessly across xUnit, NUnit, and MSTest without reflection or dynamic proxies.
  • Asynchronous Execution Guarantees: Handlers returning ValueTask<T> prevent thread-pool starvation and eliminate deadlock risks across synchronization contexts via internal ConfigureAwait(false).
  • Stryker.NET Mutation Quality Gate: Hard threshold requiring $\ge 98%$ mutation score across runtime packages:
Project Total Mutants Mutants Killed Mutants Survived Mutation Score Gate Status
EricksonLopez.Mediator (Core) 48 48 0 100.00% โœ… HIGH
EricksonLopez.Mediator.Testing 52 52 0 100.00% โœ… HIGH
EricksonLopez.Mediator.OpenTelemetry 58 58 0 100.00% โœ… HIGH
EricksonLopez.Mediator.Polly 21 21 0 100.00% โœ… HIGH
EricksonLopez.Mediator.FluentValidation 19 19 0 100.00% โœ… HIGH
EricksonLopez.Mediator.RateLimiting 11 11 0 100.00% โœ… HIGH
EricksonLopez.Mediator.AspNetCore 8 8 0 100.00% โœ… HIGH
EricksonLopez.Mediator.Result 0 0 0 100.00% โœ… HIGH
EricksonLopez.Mediator.Generator 877 798 79 91.00%+ โœ… PASS
# Run full mutation testing suite
dotnet stryker --config-file stryker-config.json

See the full Mutation Testing Score Report for detailed metrics and configuration.


โšก Performance Benchmarks

All benchmarks are measured using BenchmarkDotNet v0.15.8 on modern x64 architecture.

Environment: .NET 10.0.100, X64 RyuJIT AVX-512, Windows 11, BenchmarkDotNet v0.15.8

Dispatch Pipeline Latency & Memory Allocations

Benchmark scenario: 1 Dispatched Request traversing 0, 1, and 5 Pipeline Behaviors (Logging, Validation, Metrics, Telemetry, Rate Limiting)

Library / Method Mean (ns) Error StdDev Ratio Gen0 Allocated Alloc Ratio
Direct Call (Baseline) 1.12 ns ยฑ0.012 ns ยฑ0.010 ns 1.00 - 0 B 1.00
EricksonLopez.Mediator (0 Behaviors) 1.84 ns ยฑ0.018 ns ยฑ0.016 ns 1.64 - 0 B 1.00
EricksonLopez.Mediator (1 Behavior) 3.45 ns ยฑ0.025 ns ยฑ0.022 ns 3.08 - 0 B 1.00
EricksonLopez.Mediator (5 Behaviors) 9.12 ns ยฑ0.081 ns ยฑ0.072 ns 8.14 - 0 B 1.00
martinothamar/Mediator (0 Behaviors) 2.10 ns ยฑ0.022 ns ยฑ0.019 ns 1.88 - 0 B 1.00
MediatR v13+ (0 Behaviors) 24.60 ns ยฑ0.180 ns ยฑ0.165 ns 21.96 0.0076 48 B โˆž
MediatR v13+ (1 Behavior) 58.20 ns ยฑ0.420 ns ยฑ0.390 ns 51.96 0.0178 112 B โˆž

Key Performance Drivers

  1. Compile-Time Monomorphization: The Roslyn Source Generator generates a flat C# pattern match switch table, eliminating all reflection (MethodInfo.Invoke), dynamic delegates, and dynamic method invokers.
  2. Zero-Allocation Struct Continuations: Unboxed struct INext<TResponse> continuations are inlined by RyuJIT and Native AOT compilers, eliminating delegate closures and heap allocations.
  3. ValueTask<TResponse> Throughout: Synchronous completions and cached query results do not allocate Task objects on the managed heap.

๐ŸŒ Compatibility & Technical Matrix

Target Framework & Native AOT Compatibility

Package .NET 8.0 LTS .NET 9.0 STS .NET 10.0 LTS .NET Standard 2.0 NativeAOT Trimmable Notes
EricksonLopez.Mediator โœ… โœ… โœ… โ€” โœ… Compatible โœ… Trimmable Zero reflection in hot path; 0 trim warnings
EricksonLopez.Mediator.Generator โ€” โ€” โ€” โœ… N/A N/A Build-time Roslyn Incremental Source Generator & Analyzer
EricksonLopez.Mediator.AspNetCore โœ… โœ… โœ… โ€” โš ๏ธ Partial โš ๏ธ Partial AOT-001: MapCommand/MapQuery use reflection for Minimal API route binding
EricksonLopez.Mediator.Caching โœ… โœ… โœ… โ€” โœ… Compatible โœ… Trimmable High-performance query caching and invalidation with zero reflection
EricksonLopez.Mediator.FluentValidation โœ… โœ… โœ… โ€” โš ๏ธ Compatible โš ๏ธ Compatible Behavior execution is AOT-safe; dynamic assembly scanning uses [RequiresUnreferencedCode]
EricksonLopez.Mediator.OpenTelemetry โœ… โœ… โœ… โ€” โœ… Compatible โœ… Trimmable Pre-cached type metadata in closed-generic static fields (ADR-030)
EricksonLopez.Mediator.Polly โœ… โœ… โœ… โ€” โš ๏ธ Deprecated โš ๏ธ Deprecated Deprecated (ADR-036); migrate to EricksonLopez.Resilience.Mediator
EricksonLopez.Mediator.RateLimiting โœ… โœ… โœ… โ€” โœ… Compatible โœ… Trimmable Built directly on System.Threading.RateLimiting
EricksonLopez.Mediator.Result โœ… โœ… โœ… โ€” โœ… Compatible โœ… Trimmable Zero-allocation struct result factory bridging
EricksonLopez.Mediator.Testing โœ… โœ… โœ… โ€” Test Doubles Test Doubles In-memory FakeMediator and DelegateNext for unit test projects

Notification Publish Strategies Matrix

Strategy Ordering Concurrency Model Exception Handling Recommended Scenario
Sequential (Default) Sequential Single thread Fails fast on first exception Default business workflows where execution order matters
Parallel Non-deterministic Concurrent (Task.WhenAll) Aggregates all exceptions High-throughput notification fan-out
SequentialAggregateExceptions Sequential Single thread Collects all exceptions, runs all handlers Critical audit and multi-step notification pipelines

๐Ÿ›ก๏ธ Target Framework & Lifecycle Policy: First-class multi-targeting across .NET 10 (Modern LTS), .NET 9 (STS), and .NET 8 (Enterprise LTS) โ€” along with .NET Standard 2.0 for Roslyn analyzers and source generators โ€” is actively maintained. Full backward compatibility is guaranteed until Microsoft officially reaches End-of-Life (EOL) for .NET 8 and .NET 9 in November 2026, at which milestone the ecosystem will transition to .NET 10 and .NET 11.


๐Ÿ›๏ธ Architecture & Design Principles

Compile-Time Monomorphization Pipeline & Execution Flow

flowchart TD
    Client["Client / Minimal API / Controller"] -->|Send(request, ct)| Sender["ISender / IMediator"]
    Sender -->|Compile-Time Monomorphized Switch| Dispatcher["GeneratedMediator.g.cs"]
    
    subgraph Pipeline["Zero-Allocation Pipeline (struct INext&lt;TResponse&gt;)"]
        Dispatcher --> B1["Behavior 0: OpenTelemetry Tracing"]
        B1 -->|next.InvokeAsync()| B2["Behavior 1: Rate Limiting"]
        B2 -->|next.InvokeAsync()| B3["Behavior 2: Caching (Query)"]
        B3 -->|Cache Hit| FastReturn["Return ValueTask&lt;TResponse&gt; (0 Alloc)"]
        B3 -->|Cache Miss / Command| B4["Behavior 3: FluentValidation"]
        B4 -->|Validation Failure| ShortCircuit["IResultFactory.CreateFailure()"]
        B4 -->|Validation Pass| Handler["Concrete ICommandHandler / IQueryHandler"]
    end
    
    Handler -->|ValueTask&lt;TResponse&gt;| Caller["Return to Caller"]
    ShortCircuit -->|Failure Result| Caller
    FastReturn --> Caller

Request / Response Pipeline Sequence Flow

sequenceDiagram
    autonumber
    participant Caller as Calling Endpoint
    participant Mediator as GeneratedMediator (Monomorphized Switch)
    participant Behavior as IPipelineBehavior<TReq, TRes>
    participant Next as readonly struct INext<TRes>
    participant Handler as ICommandHandler / IQueryHandler

    Caller->>Mediator: Send(command, cancellationToken)
    Note over Mediator: Direct C# pattern match (0 Reflection)
    Mediator->>Behavior: Handle(request, nextStruct, ct)
    Behavior->>Next: InvokeAsync()
    Next->>Handler: Handle(request, ct)
    Handler-->>Next: ValueTask<TResponse>
    Next-->>Behavior: ValueTask<TResponse>
    Behavior-->>Mediator: ValueTask<TResponse>
    Mediator-->>Caller: ValueTask<TResponse>

State Machine: Request Lifecycle

stateDiagram-v2
    [*] --> Received : ISender.Send()
    Received --> Validating : [ValidateRequest] present
    Validating --> ValidationFailed : Constraint violated
    Validating --> Dispatching : All constraints passed
    ValidationFailed --> [*] : Return Failure Result / Throw MediatorValidationException
    Received --> Dispatching : No validation attribute
    Dispatching --> BehaviorPipeline : Behaviors registered
    Dispatching --> HandlerExecution : Zero behaviors
    BehaviorPipeline --> HandlerExecution : next.InvokeAsync()
    BehaviorPipeline --> ShortCircuited : IResultFactory short-circuit
    ShortCircuited --> [*] : Return Failure Result
    HandlerExecution --> Completed : ValueTask<TResponse> returned
    HandlerExecution --> ExceptionThrown : Unhandled exception
    Completed --> [*] : Response returned to caller
    ExceptionThrown --> [*] : Exception propagated

๐Ÿ›ก๏ธ Best Practices & Anti-Patterns

Scenario โŒ Avoid โœ… Recommended
CQRS Segregation Using ICommand<T> for read-only queries or IQuery<T> for state mutations Segregate reads (IQuery<T>) and writes (ICommand<T>) strictly
Pipeline Continuations Storing next as an INext<TResponse> interface variable (causes heap boxing) Constrain where TNext : struct, INext<TResponse> and call next.InvokeAsync() directly
Behavior Ordering Leaving behavior execution order unspecified or duplicating Order indices (ELM008) Explicitly assign unique, deterministic order indices via [UseBehavior(typeof(B), order: N)]
Cross-Cutting Concerns Embedding validation, logging, or retry logic directly inside handlers Encapsulate cross-cutting concerns inside reusable IPipelineBehavior<TRequest, TResponse> middleware
Handler Lifetimes Registering handlers as Transient when injecting Scoped dependencies Mark handlers with [ServiceLifetime(HandlerLifetime.Scoped)] or Singleton appropriately
Error Handling Throwing expensive exceptions for predictable business validation failures Bridge with EricksonLopez.Result and IResultFactory<TResponse> for zero-exception short-circuiting
Unit Testing Using reflection-based mocking frameworks (Moq/NSubstitute) to mock IMediator Use the official in-memory FakeMediator and DelegateNext<T> test doubles
Multi-Assembly Discovery Relying on runtime assembly scanning (Assembly.GetTypes()) Use [assembly: DiscoverHandlers(typeof(MarkerType))] for compile-time multi-assembly scanning
Native AOT Safety Calling Type.GetType() or MakeGenericMethod() inside custom handlers Rely on compile-time source generation and static closed types

โš ๏ธ Troubleshooting & Common Pitfalls

Always ensure the Roslyn Source Generator is configured as an Analyzer in your project.
If services.AddEricksonLopezMediator() or handler dispatch methods fail to resolve at compile time, verify that EricksonLopez.Mediator.Generator has OutputItemType="Analyzer" in your .csproj.

1. Handlers in External Assemblies Not Discovered (ELM001)

Symptom: The compiler raises ELM001: No handler found for request type even though the handler class exists in another project.
Cause: Roslyn Source Generators operate per-compilation assembly. Handlers declared in external referenced projects are not inspected by default.
Remediation: Declare the [DiscoverHandlers] attribute on your assembly targeting a marker type in the external project:

[assembly: DiscoverHandlers(typeof(OrdersModuleMarker))]

2. Struct Boxing in Custom Pipeline Behaviors

Symptom: Memory profilers show delegate or interface boxing allocations inside pipeline execution.
Cause: Storing the next parameter into an INext<TResponse> interface variable or passing it to an unconstrained method boxes the struct to the managed heap.
Remediation: Keep the struct generic constraint intact on the handler method:

// Correct: Zero allocation
public ValueTask<TResponse> Handle<TNext>(TRequest request, TNext next, CancellationToken cancellationToken)
    where TNext : struct, INext<TResponse>
{
    return next.InvokeAsync();
}

3. Non-Deterministic Behavior Execution Order (ELM008)

Symptom: Compiler warning ELM008: Behavior order conflict emitted during build.
Cause: Multiple behaviors registered on the same request share identical explicit order numbers.
Remediation: Assign unique sequential indices (order: 0, order: 1, order: 2):

[assembly: UseGlobalBehavior(typeof(TracingBehavior<,>), order: 0)]    // Outermost
[assembly: UseGlobalBehavior(typeof(LoggingBehavior<,>), order: 1)]    // Second
[assembly: UseGlobalBehavior(typeof(ValidationPipelineBehavior<,>), order: 2)] // Innermost

4. Minimal API Route Binding under Native AOT (AOT-001)

Symptom: Warnings IL2026: Using member RequiresUnreferencedCode when using app.MapCommand<TCommand, TResponse>() or app.MapQuery<TQuery, TResponse>() with Native AOT compilation.
Cause: ASP.NET Core Minimal API endpoint route delegate binding relies on reflection internally.
Remediation: In strictly trimmed Native AOT applications requiring zero trim warnings, dispatch through standard typed Minimal API delegate lambdas injecting ISender:

// 100% Native AOT-safe explicit delegate mapping
app.MapPost("/api/orders", static async (CreateOrderCommand command, ISender sender, CancellationToken ct) =>
{
    var id = await sender.Send(command, ct);
    return Results.Created($"/api/orders/{id}", id);
});

๐ŸŒ Part of the EricksonLopez Ecosystem

EricksonLopez.Mediator is a foundational component of the high-performance, Native AOT-first .NET ecosystem:


๐Ÿค Contributing

Contributions, issues, and feature requests are welcome!

Local Development Workflow

  1. Prerequisites: Install .NET 10.0 SDK (or .NET 8.0/9.0 SDK).
  2. Clone & Restore:
    git clone https://github.com/ericksonlopezf/dotnet-mediator.git
    cd dotnet-mediator
    dotnet restore EricksonLopez.Mediator.slnx
    
  3. Build the Solution:
    dotnet build EricksonLopez.Mediator.slnx --configuration Release
    
  4. Run the Full Test Suite:
    dotnet test EricksonLopez.Mediator.slnx --configuration Release
    
  5. Run Stryker Mutation Testing:
    dotnet stryker --config-file stryker-config.json
    

Please review our Contributing Guide, Code of Conduct, and Security Policy before submitting pull requests.


๐Ÿ“„ License

Distributed under the MIT License.
Copyright ยฉ 2026 Erickson Lopez.

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 is compatible.  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 is compatible.  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
2.0.0 0 9/24/2026
1.0.0 225 8/27/2026