SkathIO.Rogue.Abstractions 1.2.0

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

SkathIO.Rogue

NuGet CI License: MIT

AOT-safe, source-generated CQRS/mediator for .NET. Free and MIT-licensed forever.

Rogue is a drop-in alternative to MediatR built for the modern .NET ecosystem: reflection-free dispatch via Roslyn source generation, Native AOT support, zero-allocation on the hot path, and a ValueTask-based pipeline — with no commercial license required, now or ever.


Why Rogue?

MediatR martinothamar/Mediator SkathIO.Rogue
License Commercial (v13+) MIT MIT
Dispatch Reflection Source-gen (monomorphized) Source-gen (DI-resolved)
Native AOT No Yes Yes
Pipeline behaviors Open generic Open generic Open generic + [BehaviorOrder]
Streaming No No IAsyncEnumerable<T>
Migration path from MediatR — Manual One-line + compat shim

Benchmarks: Rogue meets or beats MediatR across the measured warm-path scenarios (single-handler Send, notification fan-out at N = 2 / 5 / 20, and validated Send with a real FluentValidation behavior) and leads cold-start by ~18.5×. See docs/benchmarks.md and bench/RESULTS.md for the full measured tables and methodology.


Installation

dotnet add package SkathIO.Rogue

For library authors who only need the interfaces (no source generator or DI wiring):

dotnet add package SkathIO.Rogue.Abstractions

Optional integration packages (v1.0):

dotnet add package SkathIO.Rogue.Logging
dotnet add package SkathIO.Rogue.Validation.FluentValidation

Quick start

1. Register

// Program.cs
builder.Services.AddRogue();

The source generator discovers all IQueryHandler<,>, ICommandHandler<> / ICommandHandler<,>, IEventHandler<>, and IStreamQueryHandler<,> types in your project at compile time — no assembly scanning, no reflection.

2. Define requests and handlers

// Queries return data
public record GetUserQuery(int Id) : IQuery<User>;

public class GetUserQueryHandler : IQueryHandler<GetUserQuery, User>
{
    public ValueTask<User> Handle(GetUserQuery query, CancellationToken ct)
        => ValueTask.FromResult(new User(query.Id, "Alice"));
}

// Commands signal intent
public record CreateOrderCommand(string ProductId, int Qty) : ICommand<OrderId>;

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, OrderId>
{
    public async ValueTask<OrderId> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        // ... create the order
        return new OrderId(Guid.NewGuid());
    }
}

// Events fan out to all handlers
public record OrderPlacedEvent(OrderId OrderId) : IEvent;

public class SendOrderConfirmationHandler : IEventHandler<OrderPlacedEvent>
{
    public ValueTask Handle(OrderPlacedEvent @event, CancellationToken ct)
    {
        // ... send email
        return ValueTask.CompletedTask;
    }
}

3. Send

// Inject ISender / IPublisher via DI
public class OrderController(ISender sender, IPublisher publisher)
{
    public async Task<IActionResult> PlaceOrder(PlaceOrderRequest req)
    {
        var orderId = await sender.Send(new CreateOrderCommand(req.ProductId, req.Qty));
        await publisher.Publish(new OrderPlacedEvent(orderId));
        return Ok(orderId);
    }
}

4. Stream

public record PagedProductsQuery(int PageSize) : IStreamQuery<Product>;

public class PagedProductsHandler : IStreamQueryHandler<PagedProductsQuery, Product>
{
    public async IAsyncEnumerable<Product> Handle(
        PagedProductsQuery query,
        [EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var product in FetchPagedAsync(query.PageSize, ct))
            yield return product;
    }
}

// Usage
await foreach (var product in sender.CreateStream(new PagedProductsQuery(50)))
    Console.WriteLine(product.Name);

Scoped dispatch

AddRogue() registers ISender / IMediator (and the underlying dispatcher) as Scoped — one mediator instance per request/scope, matching the standard mediator-pattern lifetime. That means ISender/IMediator must be resolved from within a scope, not from the root IServiceProvider.

For ASP.NET Core controllers/minimal-API handlers, constructor/parameter injection already resolves from the current request's scope — no extra work needed (see the Quick start example above). The failure case is code that runs outside a request scope: hosted services, background workers, and startup/seeding code.

// Wrong — throws at resolution time
public class OutboxProcessor(IServiceProvider provider) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        var sender = provider.GetRequiredService<ISender>(); // throws
        ...
    }
}

// Right — create a scope first
public class OutboxProcessor(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            using var scope = scopeFactory.CreateScope();
            var sender = scope.ServiceProvider.GetRequiredService<ISender>();
            await sender.Send(new ProcessOutboxCommand(), ct);
            await Task.Delay(TimeSpan.FromSeconds(30), ct);
        }
    }
}

Resolving ISender/IMediator from the root provider throws:

System.InvalidOperationException: Cannot resolve scoped service 'SkathIO.Rogue.ISender' from root provider.

This is standard Microsoft.Extensions.DependencyInjection scope-validation behavior for any Scoped service resolved from the root container — it applies equally with or without any optional Rogue integration package (e.g. SkathIO.Rogue.Validation.FluentValidation) installed, since it's a property of the mediator's own Scoped registration, not of any particular pipeline behavior.

Note: the .NET Generic Host only enables this scope validation (ValidateScopes) by default in the Development environment. Outside Development — the default in most deployed configurations — resolving from the root provider does not throw; it silently succeeds with an instance captured against the root container, effectively a singleton mediator that never sees per-request scoped state. That's a worse failure mode than the exception above precisely because it's silent. Create a scope regardless of environment; don't rely on the exception as your only signal.


Pipeline behaviors

Behaviors wrap every request in the pipeline — use them for logging, validation, caching, or any cross-cutting concern.

public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async ValueTask<TResponse> Handle(
        TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
        var result = await next();
        logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
        return result;
    }
}

Register open behaviors — they apply to all request types automatically:

builder.Services.AddRogue(opts =>
    opts.AddOpenBehavior(typeof(LoggingBehavior<,>)));

Control pipeline order with [BehaviorOrder] (lower = outermost, executes first):

[BehaviorOrder(-10)]  // outermost — runs before everything else
public class TracingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { ... }

[BehaviorOrder(10)]   // inner — runs just before the handler
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { ... }

Validators: zero wiring, unlike behaviors

A custom behavior sometimes needs an explicit AddOpenBehavior call, as above. A FluentValidation validator never does. SkathIO.Rogue.Validation.FluentValidation discovers every AbstractValidator<T> at compile time and registers it into DI automatically — no AddOpenBehavior, no AddValidatorsFromAssembly-style scan, nothing:

public sealed class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator() => RuleFor(x => x.Quantity).GreaterThan(0);
}
// Program.cs — unchanged. Referencing the package and writing the validator above is the entire
// contract; there is no corresponding AddRogue(o => ...) call to add.
builder.Services.AddRogue();

ValidationBehavior<TRequest, TResponse> itself is still auto-woven into the pipeline the same way LoggingBehavior is above — referencing SkathIO.Rogue.Validation.FluentValidation is enough. See Validation behavior for the full contract, an important note if you're upgrading from a version that predates validator auto-discovery, and the ExcludeAssets="analyzers" opt-out.


Performance

Rogue dispatches via source-generated, DI-resolved methods — no reflection on the hot path.

  • 0 bytes allocated on the generated concrete dispatch path (RogueDispatcher.Send{Request}) for a behavior-free, synchronously-completing handler; the portable ISender.Send<T> path boxes one ValueTask<T> by design
  • Faster than MediatR on the measured warm paths (single-handler Send, notification fan-out at N = 2 / 5 / 20) and ~19× faster cold-start — no runtime assembly scan
  • Native AOT compatible — no dynamic code generation, no Expression.Compile(), no MakeGenericMethod
  • ValueTask-based throughout — avoids unnecessary heap allocations vs Task

Full benchmark results and methodology: docs/benchmarks.md and bench/RESULTS.md.


Migration from MediatR

Most MediatR code migrates in one step:

- services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
+ services.AddRogue();

Rogue's native core is CQS-explicit (IQuery<T>, ICommand / ICommand<T>, IEvent, IStreamQuery<T>). For drop-in migration, the SkathIO.Rogue.MediatR package provides the MediatR-shaped surface (IRequest<T>, IRequestHandler<,>, INotification, INotificationHandler<>, IStreamRequest<T>, …) under the SkathIO.Rogue.Compatibility namespace, plus an AddMediatR(...) shim so existing IMediator injection points keep working. A bundled Roslyn analyzer (ROGM001–006) ships code-fixes — including rewrites to the native CQS contracts.

See the migration guide and the before/after sample.


Packages

Package Purpose
SkathIO.Rogue Core library — source generator + DI wiring + dispatcher
SkathIO.Rogue.Abstractions Interfaces only — for library authors
SkathIO.Rogue.Logging LoggingBehavior<,> — structured request/response logging (v1.0)
SkathIO.Rogue.Validation.FluentValidation ValidationBehavior<,> — FluentValidation pipeline integration (v1.0)
SkathIO.Rogue.MediatR MediatR compatibility shim — IMediator adapter (v1.0)

Requirements

  • .NET 8.0+ (or .NET Standard 2.0 for library authors)
  • C# 10+ (source generator produces modern C#)
  • Supports Native AOT on .NET 8+

Contributing

See CONTRIBUTING.md. All contributions are welcome — open an issue before starting large changes.


License

MIT — free forever, no exceptions.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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 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. 
.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 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  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 (1)

Showing the top 1 NuGet packages that depend on SkathIO.Rogue.Abstractions:

Package Downloads
SkathIO.Rogue

SkathIO.Rogue — AOT-safe, source-generated CQRS/mediator for .NET.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.0 300 7/16/2026
1.1.1 300 7/14/2026
1.1.0 293 7/13/2026
1.0.2 310 6/29/2026
1.0.1 299 6/22/2026
1.0.0 296 6/21/2026