Skip to main content

NetMediate.SourceGeneration

NetMediate.SourceGeneration is a Roslyn incremental source generator that emits handler registrations automatically at compile time. It is the standard and only supported registration path for NetMediate handlers.

Install NetMediate.SourceGeneration directly in the startup/application project. Its buildTransitive file adds the required PackageReference entries for NetMediate and GenDI.SourceGenerator. If you keep contracts in a separate project, reference NetMediate.Core there.

Installation

<PackageReference Include="NetMediate.SourceGeneration" Version="x.x.x.x">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>contentfiles; compile; runtime</PrivateAssets>
</PackageReference>

That is all for the startup/application project. dotnet add package NetMediate.SourceGeneration is enough to activate the generator and pull the required indirect dependencies.

Contracts-only projects: Use NetMediate.Core if the project only needs NetMediate contracts and should not run the generator itself.

<PackageReference Include="NetMediate.Core" Version="x.x.x" />

Activated generators

The NetMediate.SourceGeneration package activates two generators in the consuming project:

Generator DLLWhat it generates
NetMediate.SourceGeneration.dllAddNetMediate(), NetMediateGeneratedDI, NetMediateTypedExtensions, global usings
GenDI.SourceGenerator.dllAddGenDIServices() for your own [Injectable]-annotated classes

Because GenDI.SourceGenerator.dll is referenced indirectly through buildTransitive, you can use [Injectable], [ServiceInjection], and related attributes without installing a separate package in the startup project.

The package's buildTransitive/NetMediate.SourceGeneration.props file adds those PackageReference entries automatically. This keeps the startup project minimal while still provisioning the runtime and generator stack.

Usage

using GenDI;
using NetMediate;

var builder = Host.CreateApplicationBuilder();
builder.Services.AddNetMediate();

That's it. The generator discovers all concrete (non-abstract, non-generic) classes that implement one of the NetMediate handler interfaces in your project. The generated AddNetMediate() entrypoint is intentionally GenDI-first: it chains your project's AddGenDIServices() output first, then NetMediate's own AddGenDIServices() output. It does not emit legacy Register*Handler<> calls anymore.

Generated artifactCurrent role
NetMediateGeneratedDI.g.csEmits AddNetMediate() and chains application-local and NetMediate AddGenDIServices() calls
NetMediateTypedExtensions.g.csEmits strongly typed Send*Async, Notify*Async, Request*Async, and Stream*Async helper methods

The generated method is decorated with [ExcludeFromCodeCoverage] — you do not need to test it directly.

GenDI-first implementation style

Because AddNetMediate() also triggers AddGenDIServices(), the recommended style for your own implementations is:

using GenDI;
using Microsoft.Extensions.DependencyInjection;

[ServiceInjection]
public interface IInventoryGateway
{
Task ReserveAsync(string sku, CancellationToken cancellationToken);
}

[Injectable(ServiceLifetime.Scoped, Group = 20, Order = 1, Key = "primary")]
public sealed class InventoryGateway : IInventoryGateway
{
public Task ReserveAsync(string sku, CancellationToken cancellationToken) => Task.CompletedTask;
}

[Injectable(ServiceLifetime.Scoped, Group = 100, Order = 1)]
public sealed class ReserveInventoryHandler : INotificationHandler<OrderCreated>
{
[Inject] public required IInventoryGateway InventoryGateway { get; init; }
[Inject] public required ILogger<ReserveInventoryHandler> Logger { get; init; }
}

Use GenDI metadata to control ServiceLifetime, Group, Order, and Key. Use [Injectable<TService>] only when you need to force a specific non-generic contract and contract discovery does not already find [ServiceInjection]. Concrete non-generic classes that implement closed generic contracts can still use [Injectable]. Only generic/open service implementations (for example AuditBehavior<TMessage, TResponse>) should be registered manually in builder.Services for the AOT-oriented path.

Keyed handlers: The source generator handles two cases automatically:

  • Handler decorated with [Injectable(..., Key = "mykey")] → registered with the explicit key "mykey".
  • Handler with no Key → registered as a regular non-keyed service in the container. mediator.SendMyCmdAsync(command, ct) and mediator.SendMyCmdAsync(null, command, ct) are equivalent and target those non-keyed handlers.

Keyed routing is handled by GenDI keyed service registrations; NetMediate only performs keyed dispatch resolution at runtime.

AOT / NativeAOT

The source-generator path is fully AOT-safe — no reflection, no MakeGenericType, no assembly scanning. See Native AOT Support for the complete compatibility guide.

Controlling registration order with GenDI metadata

Apply Group + Order on [Injectable] to control registration order. Lower values are registered first.

[Injectable(ServiceLifetime.Scoped, Group = 10, Order = 1)]
public sealed class AuditHandler : ICommandHandler&lt;AuditCommand&gt; { ... }

[Injectable(ServiceLifetime.Scoped, Group = 10, Order = 2)]
public sealed class MetricsHandler : ICommandHandler&lt;MetricsCommand&gt; { ... }

// No explicit Group/Order → registered last (implicit order = int.MaxValue).
[Injectable(ServiceLifetime.Scoped)]
public sealed class FallbackHandler : ICommandHandler&lt;AuditCommand&gt; { ... }

Registration order affects the pipeline wrapping order: behaviors registered earlier wrap the pipeline outermost, so they run before later-registered behaviors.

Scope: Group + Order come from GenDI metadata on the discovered implementations.

Generated namespace and AddNetMediate() discoverability

The generator places NetMediateGeneratedDI (and its AddNetMediate() extension method) in a namespace derived from your project's assembly namespace:

<YourAssemblyNamespace>.NetMediate

For C# 10 and later the generator also emits a companion NetMediateGlobalUsings.g.cs file that adds global using <YourAssemblyNamespace>.NetMediate; to the project automatically. This means AddNetMediate() is available everywhere in your project without any manual using directive.

If your project targets C# 9 or earlier, add the using directive explicitly in your entry-point file:

// Program.cs or Startup.cs
using MyApp.NetMediate; // the generated namespace

builder.Services.AddNetMediate();

Namespace selection algorithm

The generator uses the current project's assembly namespace directly — one namespace per project, resolved independently, matching the same per-project strategy used by GenDI. For example:

Assembly namespaceGenerated namespace
Acme.WebAcme.Web.NetMediate
Acme.ApiAcme.Api.NetMediate
MyAppMyApp.NetMediate

Each project always gets its own isolated namespace. No cross-project or cross-build state is involved.

Only the core NetMediate assembly is skipped automatically (AssemblyName == "NetMediate"). Other NetMediate.* assemblies are not auto-skipped.

Typed dispatch extension methods

Starting with this release, the source generator also emits a second file — NetMediateTypedExtensions.g.cs — that contains named, fully-typed extension methods for every message type it discovers in your project. These methods are AOT-safe and reflection-free: they call the concrete IMediator overloads directly with both type arguments resolved at compile time.

Generated method names

Handler interfaceVerbExample generated method
ICommandHandler&lt;MyCmd&gt;SendSendMyCmdAsync(...)
INotificationHandler&lt;MyEvt&gt;NotifyNotifyMyEvtAsync(...)
IRequestHandler&lt;MyQuery, MyResponse&gt;RequestRequestMyQueryAsync(...)
IStreamHandler&lt;MyFeed, MyItem&gt;StreamStreamMyFeedAsync(...)

Overloads generated per message type

Commands and notifications receive four overloads:

// 1. Key-less dispatch (passes null and uses the non-keyed handlers)
Task SendMyCmdAsync(this IMediator mediator, MyCmd message, CancellationToken ct = default);

// 2. Explicit routing key
Task SendMyCmdAsync(this IMediator mediator, object? key, MyCmd message, CancellationToken ct = default);

// 3. Batch dispatch (key-less)
Task SendMyCmdAsync(this IMediator mediator, IEnumerable<MyCmd> messages, CancellationToken ct = default);

// 4. Batch dispatch with explicit key
Task SendMyCmdAsync(this IMediator mediator, object? key, IEnumerable<MyCmd> messages, CancellationToken ct = default);

Requests receive two overloads:

Task<MyResponse> RequestMyQueryAsync(this IMediator mediator, MyQuery message, CancellationToken ct = default);
Task<MyResponse> RequestMyQueryAsync(this IMediator mediator, object? key, MyQuery message, CancellationToken ct = default);

Streams receive two overloads:

IAsyncEnumerable<MyItem> StreamMyFeedAsync(this IMediator mediator, MyFeed message, CancellationToken ct = default);
IAsyncEnumerable<MyItem> StreamMyFeedAsync(this IMediator mediator, object? key, MyFeed message, CancellationToken ct = default);

Usage example

With the global using emitted by the generator (C# 10+) you can write:

// Instead of: mediator.Request<MyQuery, MyResponse>(new MyQuery(id), ct)
var result = await mediator.RequestMyQueryAsync(new MyQuery(id), ct);

// Batch send:
await mediator.SendMyCmdAsync(commands, ct);

// Keyed dispatch:
var value = await mediator.RequestMyQueryAsync("tenant-a", new MyQuery(id), ct);

Conflict resolution

If two different message types share the same simple name (e.g., Commands.PingCommand and Events.PingCommand), the generator disambiguates by deriving the method name from the fully-qualified type name with dots removed:

// Instead of the conflicting SendPingCommandAsync:
Task SendCommandsPingCommandAsync(...);
Task SendEventsPingCommandAsync(...);

Deduplication

Multiple handlers for the same message type (e.g., one default and one keyed handler) produce only one set of extension methods — the keyed handler is reached via the object? key overload.