Interlink
Guides & References

Documentation โ€ข Interlink

A lightweight and minimal mediator library for .NET. Interlink helps you decouple your application using request/response and notification patterns with simple, clean code.

Introduction

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.


Features

  • ๐Ÿงฉ Simple mediator pattern for request/response
  • ๐Ÿ” Publish/Subscribe notification system
  • ๐Ÿ”ง Pipeline behaviors (logging, validation, etc.)
  • ๐Ÿง  Clean separation of concerns via handlers
  • ๐Ÿช Dependency injection support out of the box
  • ๐Ÿ”„ Pre and Post Processors
  • ๐Ÿ” Assembly scanning for automatic registration
  • ๐Ÿงช Custom service factory injection
  • ๐Ÿ”„ Pipeline ordering via attributes or configuration
  • ๐Ÿšจ Dedicated HandlerNotFoundException
  • โœ… Compatible with .NET Standard 2.0+ to .NET 10
  • ๐Ÿ“ฆ Optional packages: Logging, Validation, ASP.NET Core, Analyzer

Installation

Install 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

Dependency Injection Setup

Register Interlink in Program.cs:

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();

Requests & Handlers

A request represents a single operation that expects a response. Implement IRequest<TResponse> and its corresponding handler.

GetAllPets.cs
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:

PetController.cs
[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.


Notifications & Pub/Sub

Use notifications when multiple handlers need to react to a domain event.

UserCreated.cs
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:

AccountService.cs
public class AccountService(IPublisher publisher)
{
    public async Task RegisterUser(string username)
    {
        // Save to DB...
        await publisher.Publish(new UserCreated(username));
    }
}

Pipeline Behaviors

Pipeline behaviors wrap the handler and can run logic before and after it (logging, validation, timing, etc.).

TimingBehavior.cs
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 & Post Processors

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;
    }
}

Built-in Logging Behavior

dotnet add package Interlink.Extensions.Logging
builder.Services.AddInterlinkLogging();

Registers LoggingBehavior<TRequest, TResponse> which logs request start, successful completion + elapsed time, and exceptions.


FluentValidation Integration

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).


ASP.NET Core Integration

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.


Analyzer (Missing Handler Detection)

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.


API Overview

Core Contracts

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);
}

Sender & Publisher

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;
}

Pipeline & Processors

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);
}