A lightweight and minimal mediator library for .NET. Interlink helps you decouple your application using request/response and notification patterns with simple, clean code.
Interlink is a lightweight and minimal mediator library for .NET. It helps you decouple your application using request/response and notification patterns with simple, clean code.
Built with simplicity and performance in mind, it streamlines communication between components while keeping a clean architecture โ ideal for CQRS, Clean Architecture, and modular designs.
HandlerNotFoundExceptionInstall the core package via the .NET CLI:
dotnet add package Interlink
Optional packages:
dotnet add package Interlink.Extensions.Logging
dotnet add package Interlink.Extensions.Validation
dotnet add package Interlink.AspNetCore
dotnet add package Interlink.Analyzers
Register Interlink in Program.cs:
// Basic registration
builder.Services.AddInterlink();
// Scan a specific assembly
builder.Services.AddInterlink(typeof(MyHandler).Assembly);
// With behaviors and custom factory
builder.Services.AddInterlink(options =>
{
options.AddBehavior(typeof(LoggingBehavior<,>), order: 0);
options.AddBehavior(typeof(ValidationBehavior<,>), order: 1);
options.ServiceFactory = type => /* your custom resolver */;
}, typeof(MyHandler).Assembly);
// With optional extension packages
builder.Services.AddInterlink(typeof(MyHandler).Assembly);
builder.Services.AddInterlinkLogging();
builder.Services.AddInterlinkValidation(typeof(MyValidator).Assembly);
builder.Services.AddInterlinkAspNetCore();
A request represents a single operation that expects a response. Implement IRequest<TResponse> and its corresponding handler.
public class GetAllPets
{
public sealed record Query : IRequest<List<string>>;
public sealed class Handler : IRequestHandler<Query, List<string>>
{
public Task<List<string>> Handle(Query request, CancellationToken cancellationToken)
{
var pets = new List<string> { "Dog", "Cat", "Fish" };
return Task.FromResult(pets);
}
}
}
Send the request with ISender:
[ApiController]
[Route("api/[controller]")]
public class PetController(ISender sender) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAllPets(CancellationToken cancellationToken)
{
var pets = await sender.Send(new GetAllPets.Query(), cancellationToken);
return Ok(pets);
}
}
If no handler is registered, Send throws HandlerNotFoundException.
Use notifications when multiple handlers need to react to a domain event.
public sealed class UserCreated(string userName) : INotification
{
public string UserName { get; } = userName;
}
public sealed class SendWelcomeEmail : INotificationHandler<UserCreated>
{
public Task Handle(UserCreated notification, CancellationToken cancellationToken)
{
Console.WriteLine($"Welcome email sent to {notification.UserName}");
return Task.CompletedTask;
}
}
Publish with IPublisher:
public class AccountService(IPublisher publisher)
{
public async Task RegisterUser(string username)
{
// Save to DB...
await publisher.Publish(new UserCreated(username));
}
}
Pipeline behaviors wrap the handler and can run logic before and after it (logging, validation, timing, etc.).
public sealed class TimingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
var response = await next(cancellationToken);
sw.Stop();
Console.WriteLine($"{typeof(TRequest).Name} took {sw.ElapsedMilliseconds} ms");
return response;
}
}
Ordering (lower value runs first / outermost):
[PipelineOrder(1)]
public sealed class FirstBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { ... }
[PipelineOrder(2)]
public sealed class SecondBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { ... }
// Or via registration
options.AddBehavior(typeof(FirstBehavior<,>), order: 1);
options.AddBehavior(typeof(SecondBehavior<,>), order: 2);
Pre-processors run before the pipeline. Post-processors run after a successful pipeline.
They are discovered automatically by AddInterlink().
public sealed class MyRequestPreProcessor : IRequestPreProcessor<GetAllPets.Query>
{
public Task Process(GetAllPets.Query request, CancellationToken cancellationToken)
{
Console.WriteLine("[Pre] GetAllPets");
return Task.CompletedTask;
}
}
public sealed class MyRequestPostProcessor : IRequestPostProcessor<GetAllPets.Query, List<string>>
{
public Task Process(GetAllPets.Query request, List<string> response, CancellationToken cancellationToken)
{
Console.WriteLine($"[Post] returned {response.Count} pets");
return Task.CompletedTask;
}
}
dotnet add package Interlink.Extensions.Logging
builder.Services.AddInterlinkLogging();
Registers LoggingBehavior<TRequest, TResponse> which logs request start, successful completion + elapsed time, and exceptions.
dotnet add package Interlink.Extensions.Validation
builder.Services.AddInterlinkValidation(typeof(CreateUserValidator).Assembly);
public sealed class CreateUserValidator : AbstractValidator<CreateUser.Command>
{
public CreateUserValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
}
}
Throws FluentValidation.ValidationException on failure (mapped to 400 by the ASP.NET Core filter).
dotnet add package Interlink.AspNetCore
builder.Services.AddControllers();
builder.Services.AddInterlinkAspNetCore(); // adds InterlinkExceptionFilter
| Exception | HTTP Status | Response |
|---|---|---|
HandlerNotFoundException |
404 | ProblemDetails |
ValidationException* |
400 | ValidationProblemDetails |
* FluentValidation support is optional and detected at runtime.
dotnet add package Interlink.Analyzers
Produces diagnostic ILINK001 (warning) when a type implements IRequest<TResponse>
but no corresponding IRequestHandler<TRequest, TResponse> is found in the compilation.
public interface IRequest<out TResponse> { }
public interface IRequestHandler<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotification { }
public interface INotificationHandler<in TNotification>
where TNotification : INotification
{
Task Handle(TNotification notification, CancellationToken cancellationToken);
}
public interface ISender
{
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
}
public interface IPublisher
{
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification;
}
public delegate Task<TResponse> RequestHandlerDelegate<TResponse>(CancellationToken cancellationToken = default);
public interface IPipelineBehavior<in TRequest, TResponse>
where TRequest : notnull
{
Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}
public interface IRequestPreProcessor<in TRequest> where TRequest : notnull
{
Task Process(TRequest request, CancellationToken cancellationToken);
}
public interface IRequestPostProcessor<in TRequest, in TResponse> where TRequest : notnull
{
Task Process(TRequest request, TResponse response, CancellationToken cancellationToken);
}