GMana.Bus 0.0.2

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

GMana.Bus

Lightweight in-process request, notification, and pipeline dispatching for .NET applications.

GMana.Bus is useful when you want a small mediator-style abstraction for CQRS, application commands, queries, domain notifications, and cross-cutting request behaviors such as logging, validation, caching, or transactions.

Features

  • Request/response dispatching through IRequest<TResponse> and IRequestHandler<TRequest, TResponse>
  • Command dispatching with no payload through IRequest, IRequestHandler<TRequest>, and Unit
  • In-process notifications with any number of INotificationHandler<TNotification> handlers
  • Open-generic request pipeline behaviors
  • Assembly scanning for handlers
  • Microsoft dependency injection integration
  • ValueTask-based APIs

Install

dotnet add package GMana.Bus

Quick Start

Register the bus and scan the assembly that contains your handlers:

using GMana.Bus;

builder.Services.AddGManaBus(typeof(Program).Assembly);

Create a request and handler:

public sealed record GetProductQuery(Guid Id) : IRequest<ProductDto?>;

public sealed class GetProductQueryHandler
    : IRequestHandler<GetProductQuery, ProductDto?>
{
    public ValueTask<ProductDto?> Handle(
        GetProductQuery request,
        CancellationToken cancellationToken)
    {
        ProductDto? product = null;

        // Load the product here.

        return ValueTask.FromResult(product);
    }
}

Inject ISender where you need to execute requests:

public sealed class ProductsEndpoint(ISender sender)
{
    public async Task<IResult> Get(Guid id, CancellationToken cancellationToken)
    {
        ProductDto? product = await sender.Send(
            new GetProductQuery(id),
            cancellationToken);

        return product is null ? Results.NotFound() : Results.Ok(product);
    }
}

Registration

AddGManaBus scans one or more assemblies for request and notification handlers. Handlers are registered as scoped services. ISender, IPublisher, and the internal dispatcher are also scoped.

builder.Services.AddGManaBus(
    typeof(Program).Assembly,
    typeof(SomeApplicationHandler).Assembly);

Each request type must have exactly one handler. Registering multiple handlers for the same request throws during startup. Notifications may have zero, one, or many handlers.

Requests

A request represents work that returns a value:

public sealed record CreateOrderCommand(Guid CustomerId) : IRequest<Guid>;

public sealed class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, Guid>
{
    public ValueTask<Guid> Handle(
        CreateOrderCommand request,
        CancellationToken cancellationToken)
    {
        Guid orderId = Guid.NewGuid();

        // Create the order here.

        return ValueTask.FromResult(orderId);
    }
}

Send the request with ISender:

Guid orderId = await sender.Send(
    new CreateOrderCommand(customerId),
    cancellationToken);

If no handler is registered for the request type, Send throws an InvalidOperationException.

Commands Without a Response

Use IRequest when a command has no response payload. The bus represents the response as Unit.

public sealed record ArchiveOrderCommand(Guid OrderId) : IRequest;

public sealed class ArchiveOrderCommandHandler
    : IRequestHandler<ArchiveOrderCommand>
{
    public ValueTask<Unit> Handle(
        ArchiveOrderCommand request,
        CancellationToken cancellationToken)
    {
        // Archive the order here.

        return ValueTask.FromResult(Unit.Value);
    }
}
await sender.Send(new ArchiveOrderCommand(orderId), cancellationToken);

Notifications

Notifications represent fire-and-forget in-process events. They can have multiple handlers.

public sealed record ProductCreatedNotification(Guid ProductId) : INotification;

public sealed class ProductCreatedNotificationHandler
    : INotificationHandler<ProductCreatedNotification>
{
    public ValueTask Handle(
        ProductCreatedNotification notification,
        CancellationToken cancellationToken)
    {
        // React to the notification here.

        return ValueTask.CompletedTask;
    }
}

Inject IPublisher and publish the notification:

await publisher.Publish(
    new ProductCreatedNotification(productId),
    cancellationToken);

Notification handlers run sequentially in dependency injection registration order. Publishing a notification with no registered handlers completes successfully.

Pipeline Behaviors

Pipeline behaviors wrap request handlers. They are only applied to requests, not notifications.

Register open-generic behaviors with AddPipelineBehavior:

builder.Services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
builder.Services.AddPipelineBehavior(typeof(ValidationBehavior<,>));

Behaviors run in registration order. The first registered behavior is the outermost behavior.

LoggingBehavior
  ValidationBehavior
    RequestHandler
public sealed class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async ValueTask<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        Console.WriteLine($"Handling {typeof(TRequest).Name}");

        TResponse response = await next(request, cancellationToken);

        Console.WriteLine($"Handled {typeof(TRequest).Name}");
        return response;
    }
}

Pipeline behaviors should call next at most once and should not call it concurrently.

Behavior Recipes

Logging

using System.Diagnostics;
using GMana.Bus;

public sealed class LoggingBehavior<TRequest, TResponse>(
    ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async ValueTask<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        string requestName = typeof(TRequest).Name;
        long startTimestamp = Stopwatch.GetTimestamp();

        logger.LogInformation("Handling {RequestName}", requestName);

        try
        {
            TResponse response = await next(request, cancellationToken);

            logger.LogInformation(
                "Handled {RequestName} in {Elapsed}ms",
                requestName,
                Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds);

            return response;
        }
        catch (Exception exception)
        {
            logger.LogError(
                exception,
                "Handler {RequestName} threw after {Elapsed}ms",
                requestName,
                Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds);

            throw;
        }
    }
}
builder.Services.AddPipelineBehavior(typeof(LoggingBehavior<,>));

Validation

This example uses FluentValidation and validates only request types that have registered validators.

using FluentValidation;
using FluentValidation.Results;
using GMana.Bus;

public sealed class ValidationBehavior<TRequest, TResponse>(
    IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IValidator<TRequest>[] validators = validators.ToArray();

    public async ValueTask<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        if (validators.Length == 0)
        {
            return await next(request, cancellationToken);
        }

        ValidationContext<TRequest> context = new(request);
        List<ValidationFailure> failures = [];

        foreach (IValidator<TRequest> validator in validators)
        {
            ValidationResult result = await validator.ValidateAsync(context, cancellationToken);
            failures.AddRange(result.Errors);
        }

        if (failures.Count > 0)
        {
            throw new ValidationException(failures);
        }

        return await next(request, cancellationToken);
    }
}
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
builder.Services.AddPipelineBehavior(typeof(ValidationBehavior<,>));

Caching

Use an opt-in marker interface when only some requests should be cached.

public interface ICacheable
{
    string CacheKey { get; }
    TimeSpan? Expiration => null;
}
using GMana.Bus;
using Microsoft.Extensions.Caching.Hybrid;

public sealed class CachingBehavior<TRequest, TResponse>(HybridCache cache)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public ValueTask<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        if (request is not ICacheable cacheable)
        {
            return next(request, cancellationToken);
        }

        HybridCacheEntryOptions? options = cacheable.Expiration is { } expiration
            ? new HybridCacheEntryOptions { Expiration = expiration }
            : null;

        return cache.GetOrCreateAsync(
            cacheable.CacheKey,
            (Request: request, Next: next),
            static (state, ct) => state.Next(state.Request, ct),
            options,
            cancellationToken: cancellationToken);
    }
}
builder.Services.AddHybridCache();
builder.Services.AddPipelineBehavior(typeof(CachingBehavior<,>));

Transactions

Use an opt-in marker interface when only some commands should be wrapped in a transaction.

public interface ITransactional;
using GMana.Bus;
using Microsoft.EntityFrameworkCore.Storage;

public sealed class TransactionBehavior<TRequest, TResponse>(AppDbContext db)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public ValueTask<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TRequest, TResponse> next,
        CancellationToken cancellationToken)
    {
        return request is ITransactional
            ? HandleTransactional(next, request, cancellationToken)
            : next(request, cancellationToken);
    }

    private async ValueTask<TResponse> HandleTransactional(
        RequestHandlerDelegate<TRequest, TResponse> next,
        TRequest request,
        CancellationToken cancellationToken)
    {
        await using IDbContextTransaction tx = await db.Database
            .BeginTransactionAsync(cancellationToken);

        try
        {
            TResponse response = await next(request, cancellationToken);
            await tx.CommitAsync(cancellationToken);
            return response;
        }
        catch
        {
            await tx.RollbackAsync(cancellationToken);
            throw;
        }
    }
}
builder.Services.AddPipelineBehavior(typeof(TransactionBehavior<,>));

Design Notes

  • This is an in-process dispatcher. It is not a message broker or queue.
  • Request handler lookup uses the runtime request type.
  • A request has one handler. A notification can have many handlers.
  • Notification handlers run sequentially.
  • Pipeline behaviors apply only to requests.
  • All handlers and behaviors are resolved from the current dependency injection scope.
Product Compatible and additional computed target framework versions.
.NET 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
0.0.2 123 4/24/2026
0.0.1 108 4/24/2026