Mediocr 0.1.0-alpha.4

This is a prerelease version of Mediocr.
dotnet add package Mediocr --version 0.1.0-alpha.4
                    
NuGet\Install-Package Mediocr -Version 0.1.0-alpha.4
                    
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="Mediocr" Version="0.1.0-alpha.4">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mediocr" Version="0.1.0-alpha.4" />
                    
Directory.Packages.props
<PackageReference Include="Mediocr">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
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 Mediocr --version 0.1.0-alpha.4
                    
#r "nuget: Mediocr, 0.1.0-alpha.4"
                    
#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 Mediocr@0.1.0-alpha.4
                    
#: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=Mediocr&version=0.1.0-alpha.4&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Mediocr&version=0.1.0-alpha.4&prerelease
                    
Install as a Cake Tool

Mediocr

Not great, not terrible

A minimal Mediator with automatic handler registration via source generation.

NuGet License: MIT

Mediocr provides a lightweight mediator pattern implementation with automatic request handler registration using C# source generators. No reflection, no startup scanning, just compile-time code generation.

Installation

dotnet add package Mediocr
dotnet add package Mediocr.Interfaces

Or via Package Manager:

Install-Package Mediocr
Install-Package Mediocr.Interfaces

Quick Start

Define a Request

using Mediocr.Interfaces;

namespace MyApp.Requests;

public class GetUserRequest : IRequest<UserDto>
{
    public int UserId { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Create a Handler

using Mediocr.Interfaces;

namespace MyApp.Handlers;

public class GetUserRequestHandler : IRequestHandler<GetUserRequest, UserDto>
{
    private readonly IUserRepository _repository;
    
    public GetUserRequestHandler(IUserRepository repository)
    {
        _repository = repository;
    }
    
    public async Task<UserDto> Handle(GetUserRequest request, CancellationToken cancellationToken)
    {
        var user = await _repository.GetByIdAsync(request.UserId, cancellationToken);
        
        return new UserDto
        {
            Id = user.Id,
            Name = user.Name,
            Email = user.Email
        };
    }
}

Register Handlers

The source generator automatically creates an extension method:

using Mediocr.Interfaces;

var builder = WebApplication.CreateBuilder(args);

// Register all handlers automatically!
builder.Services.AddMediocrHandlers();

var app = builder.Build();

Resolve and Execute

public class UserController : ControllerBase
{
    private readonly IRequestHandler<GetUserRequest, UserDto> _handler;
    
    public UserController(IRequestHandler<GetUserRequest, UserDto> handler)
    {
        _handler = handler;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var request = new GetUserRequest { UserId = id };
        var result = await _handler.Handle(request, HttpContext.RequestAborted);
        return Ok(result);
    }
}

How It Works

Source Generation

When you build your project, Mediocr's source generator:

  1. Scans your code for classes implementing IRequestHandler<TInput, TOutput>
  2. Generates an extension method AddMediocrHandlers()
  3. Registers all handlers as scoped services in the DI container

Generated Code Example

For the handlers above, Mediocr generates:

// <auto-generated/>
using Microsoft.Extensions.DependencyInjection;

namespace Mediocr.Interfaces
{
    public static class MediocRServiceCollectionExtensions
    {
        public static IServiceCollection AddMediocrHandlers(this IServiceCollection services)
        {
            services.AddScoped<global::Mediocr.Interfaces.IRequestHandler<global::MyApp.Requests.GetUserRequest, global::MyApp.Requests.UserDto>, 
                              global::MyApp.Handlers.GetUserRequestHandler>();

            return services;
        }
    }
}

Viewing Generated Code

To see the generated code in your project:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)/GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Generated files appear in: obj/GeneratedFiles/Mediocr/Mediocr.RequestHandlerGenerator/

Advanced Usage

Multiple Handlers Per Class

A single class can implement multiple handler interfaces:

public class UserCommandHandlers : 
    IRequestHandler<CreateUserCommand, int>,
    IRequestHandler<UpdateUserCommand, bool>
{
    public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // Create user logic
        return newUserId;
    }
    
    public async Task<bool> Handle(UpdateUserCommand request, CancellationToken cancellationToken)
    {
        // Update user logic
        return true;
    }
}

Both handlers are automatically registered.

Generic Response Types

Handlers can return any type, including collections and complex generics:

// List response
public class GetUsersRequest : IRequest<List<UserDto>> { }

public class GetUsersHandler : IRequestHandler<GetUsersRequest, List<UserDto>>
{
    public async Task<List<UserDto>> Handle(GetUsersRequest request, CancellationToken cancellationToken)
    {
        return await _repository.GetAllAsync();
    }
}

// Dictionary response
public class GetUserPreferencesRequest : IRequest<Dictionary<string, string>> { }

// Tuple response
public class GetUserStatsRequest : IRequest<(int TotalPosts, int TotalLikes)> { }

Nested Classes

Handlers can be nested within other classes:

namespace MyApp.Features.Users;

public static class GetUser
{
    public class Query : IRequest<Result> 
    {
        public int UserId { get; set; }
    }
    
    public class Result 
    {
        public string Name { get; set; }
    }
    
    public class Handler : IRequestHandler<Query, Result>
    {
        public async Task<Result> Handle(Query request, CancellationToken cancellationToken)
        {
            // Handler logic
        }
    }
}

Record Types

Works seamlessly with C# records:

public record GetUserQuery(int UserId) : IRequest<UserDto>;

public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> Handle(GetUserQuery request, CancellationToken cancellationToken)
    {
        // Handler logic
    }
}

Service Lifetime

All handlers are registered with Scoped lifetime by default.

TODO: Extend this to other lifetime types

Testing

Unit Testing Handlers

Handlers are plain classes and easy to test:

[Fact]
public async Task GetUserHandler_ReturnsUser_WhenUserExists()
{
    // Arrange
    var mockRepo = new Mock<IUserRepository>();
    mockRepo.Setup(r => r.GetByIdAsync(1, default))
            .ReturnsAsync(new User { Id = 1, Name = "John" });
    
    var handler = new GetUserRequestHandler(mockRepo.Object);
    var request = new GetUserRequest { UserId = 1 };
    
    // Act
    var result = await handler.Handle(request, CancellationToken.None);
    
    // Assert
    Assert.Equal(1, result.Id);
    Assert.Equal("John", result.Name);
}

Integration Testing

public class UserEndpointTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    
    [Fact]
    public async Task GetUser_ReturnsOk_WithValidId()
    {
        // Arrange
        var client = _factory.CreateClient();
        
        // Act
        var response = await client.GetAsync("/api/users/1");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(user);
    }
}

Troubleshooting

  1. Check generator is loaded:

    dotnet build -v:detailed
    

    Look for "Loading analyzer" messages.

  2. Verify project references:

    <ItemGroup>
        <ProjectReference Include="../Mediocr.Interfaces/Mediocr.Interfaces.csproj" />
        <ProjectReference Include="../Mediocr/Mediocr.csproj" 
                          OutputItemType="Analyzer" 
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

Handlers Not Found

  1. Ensure handlers implement IRequestHandler<TInput, TOutput>
  2. Handlers must not be abstract
  3. Handlers must have a base list (implement at least one interface)
  4. Check namespace imports

Build Errors

If you see errors about missing types:

dotnet restore
dotnet clean
dotnet build

Examples

CQRS Pattern

// Command (changes state)
public class CreateOrderCommand : IRequest<int>
{
    public string CustomerName { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _orders;
    
    public async Task<int> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
    {
        var order = new Order
        {
            CustomerName = command.CustomerName,
            Items = command.Items
        };
        
        await _orders.AddAsync(order);
        await _orders.SaveChangesAsync();
        
        return order.Id;
    }
}

// Query (reads state)
public class GetOrderQuery : IRequest<OrderDto>
{
    public int OrderId { get; set; }
}

public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    private readonly IOrderRepository _orders;
    
    public async Task<OrderDto> Handle(GetOrderQuery query, CancellationToken cancellationToken)
    {
        var order = await _orders.GetByIdAsync(query.OrderId);
        
        return new OrderDto
        {
            Id = order.Id,
            CustomerName = order.CustomerName,
            Items = order.Items
        };
    }
}

API Controller

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IRequestHandler<GetOrderQuery, OrderDto> _getOrderHandler;
    private readonly IRequestHandler<CreateOrderCommand, int> _createOrderHandler;
    
    public OrdersController(
        IRequestHandler<GetOrderQuery, OrderDto> getOrderHandler,
        IRequestHandler<CreateOrderCommand, int> createOrderHandler)
    {
        _getOrderHandler = getOrderHandler;
        _createOrderHandler = createOrderHandler;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> GetOrder(int id)
    {
        var query = new GetOrderQuery { OrderId = id };
        var result = await _getOrderHandler.Handle(query, HttpContext.RequestAborted);
        return Ok(result);
    }
    
    [HttpPost]
    public async Task<ActionResult<int>> CreateOrder(CreateOrderCommand command)
    {
        var orderId = await _createOrderHandler.Handle(command, HttpContext.RequestAborted);
        return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
    }
}

Minimal API

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

var app = builder.Build();

app.MapGet("/api/orders/{id}", async (
    int id, 
    IRequestHandler<GetOrderQuery, OrderDto> handler) =>
{
    var query = new GetOrderQuery { OrderId = id };
    var result = await handler.Handle(query, CancellationToken.None);
    return Results.Ok(result);
});

app.MapPost("/api/orders", async (
    CreateOrderCommand command,
    IRequestHandler<CreateOrderCommand, int> handler) =>
{
    var orderId = await handler.Handle(command, CancellationToken.None);
    return Results.Created($"/api/orders/{orderId}", orderId);
});

app.Run();

License

This project is licensed under the MIT License - see the LICENSE file for details.

There are no supported framework assets in this 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.1.0-alpha.4 265 12/17/2025
0.1.0-alpha.3 257 12/17/2025
0.1.0-alpha.2 254 12/17/2025
0.1.0-alpha.1 250 12/16/2025