Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Source Generation

NuGet package Shiny.Mediator

Shiny Mediator ships an assortment of source generators that reduce boilerplate, keep the pipeline AOT/trim-safe (no runtime reflection), and improve performance. Most generators live in the core Shiny.Mediator package; endpoint mapping lives in Shiny.Mediator.AspNet. They run automatically once the relevant package is referenced — you opt individual features in with attributes or MSBuild properties.

Apply [MediatorSingleton] or [MediatorScoped] to a handler or middleware and the generator writes the DI registration for you — no manual wiring, no assembly scanning. These attributes also feed the Executers, so if you use them you need nothing else.

[MediatorSingleton]
public class MyHandler : IRequestHandler<MyRequest, MyResponse>
{
// ...
}
// in your startup - AddMediatorRegistry is generated into your assembly
services.AddShinyMediator(x => x.AddMediatorRegistry());

AddMediatorRegistry() is emitted as an extension on both ShinyMediatorBuilder and IServiceCollection, so either form works:

builder.Services.AddShinyMediator(x => x.AddMediatorRegistry());
// or
builder.Services.AddMediatorRegistry();

The only thing these attributes decide is the DI lifetime (singleton vs scoped) — they take no constructor arguments or named properties.

The attributes themselves aren’t configurable, but the generated registry can be tuned from your .csproj:

PropertyDefaultDescription
ShinyMediatorRegistryMethodNameAddMediatorRegistryRenames the generated registration method.
ShinyMediatorRegistryUseInternalClassfalseEmits the generated registry class as internal instead of public.
RootNamespaceassembly nameNamespace the generated code is placed in.
<PropertyGroup>
<ShinyMediatorRegistryMethodName>AddMyAppHandlers</ShinyMediatorRegistryMethodName>
<ShinyMediatorRegistryUseInternalClass>true</ShinyMediatorRegistryUseInternalClass>
</PropertyGroup>

This gets around a generics limitation in .NET: you can’t create a generic type at runtime without knowing its type parameters at compile time. Our v4-and-earlier implementations reached for reflection; v5+ solves it with source generation instead — far more performant, and AOT-safe.

The generator produces strongly-typed executers for your requests and stream requests, dispatching with is type tests rather than MakeGenericType.

If you need to control the generated class names (rare), two MSBuild properties are available:

PropertyDefault
ShinyMediatorRequestExecutorClassNameGeneratedMediatorRequestExecutor
ShinyMediatorStreamRequestExecutorClassNameGeneratedMediatorStreamRequestExecutor

Starting in v6.6 mediator routes all JSON through Shiny.ISerializer from the Shiny.Extensions.Serialization package. The default chain is AOT-strict — no reflection fallback — so every type mediator touches (request, response, event, scheduled-command payload, storage / cache entry) must have a JsonTypeInfo registered.

Declare a partial JsonSerializerContext with [JsonSerializable(typeof(T))] for every type that crosses the wire, and tag it with [ShinyJsonContext] so a generated [ModuleInitializer] registers it with Shiny.Json before Main runs.

using System.Text.Json.Serialization;
using Shiny;
[ShinyJsonContext]
[JsonSerializable(typeof(GetCustomerRequest))]
[JsonSerializable(typeof(CustomerResponse))]
[JsonSerializable(typeof(OrderPlacedEvent))]
internal partial class AppJsonContext : JsonSerializerContext;

Rather than hand-declaring a context, you can have mediator generate the JSON wiring for every registered handler’s contract types. It walks every [MediatorSingleton] / [MediatorScoped] handler, transitively collects request, response, command, event, and stream-request element types plus their public property types, emits a per-type JsonConverter<T> for each in-assembly contract, and ships a single __ShinyMediatorContractsJsonResolver per assembly that a [ModuleInitializer] registers with Shiny.Json before Main.

Enable it in your project file:

<PropertyGroup>
<ShinyMediatorGenerateJsonContext>true</ShinyMediatorGenerateJsonContext>
</PropertyGroup>

If a response or property is shaped like List<Customer>, Customer[], or IAsyncEnumerable<Customer>, mark the element type with [ShinyJsonInclude] — the extensions generator emits the collection-shape wrappers automatically.

[ShinyJsonInclude]
public partial class Customer { /* ... */ }
  • OpenAPI clients — when GenerateJsonConverters="true" on a MediatorHttp item, the generator emits a custom IJsonTypeInfoResolver wiring every generated model, contract, and enum (including Nullable<TEnum> and List<T> / T[] shapes) plus a [ModuleInitializer]. No [ShinyJsonContext] needed — it’s fully automatic. See OpenAPI Contract Generation.
  • Attribute-driven clients ([Http], [HttpParameter], [HttpBody]) — request and result types are user-written, so they must live in a registered [ShinyJsonContext] partial. See Request Contracts.

Creating request keys on your contracts is tedious and easy to get wrong (nulls, formatting). The [ContractKey] attribute takes a composite format string — {PropertyName}, with an optional :format for formattable types — and generates the IContractKey implementation for you. The target type must be partial.

public class MyRequest : IRequest<Something>, IContractKey
{
public string Name { get; set; }
public DateTimeOffset? Date { get; set; }
public string GetKey()
=> $"MyRequest_{Name}_{Date?.ToString("yyyyMMddHHmmss")}";
}

The format string is optional — omit it and a key is composed from all properties. See Contract Keys for how the caching, offline, and stream-replay middleware use these keys.

There are lots of libraries that generate HTTP clients (Refitter is a great option). But when you generate them inside Shiny Mediator, every HTTP call flows through the same middleware — caching, offline, resiliency, and more.

Point a <MediatorHttp> item at an OpenAPI/Swagger document and the generator produces all the request contracts and response types at build time:

<ItemGroup>
<!-- Local OpenAPI file -->
<MediatorHttp Include="specs/products-api.json"
Namespace="MyApp.Products"
ContractPostfix="HttpRequest"
GenerateJsonConverters="true"
Visible="false" />
<!-- Remote OpenAPI document -->
<MediatorHttp Include="UsersApi"
Uri="https://api.myapp.com/swagger/v1/swagger.json"
Namespace="MyApp.Users"
ContractPostfix="HttpRequest"
Visible="false" />
</ItemGroup>
// register the generated client
builder.Services.AddShinyMediator(x => x.AddGeneratedOpenApiClient());

See OpenAPI Contract Generation for the full MSBuild metadata reference, or Request Contracts if you’d rather hand-write contracts with the [Http] / [HttpParameter] / [HttpBody] attributes.

Reading attributes off methods at runtime requires deep reflection. To keep things AOT/trim-safe, a source generator hoists handler-method attributes so middleware can read them without reflection. Mark the handler partial and the generator wires it up.

public partial class MyHandler : IRequestHandler<MyRequest, MyResponse>
{
[MyCustomAttribute]
public Task<MyResponse> Handle(MyRequest request, CancellationToken ct)
{
// ...
}
}

Best of all, the generator works with your own mediator attributes. Inherit Shiny.Mediator.MediatorMiddlewareAttribute and we’ll pick it off the handlers for you.

public class MyCustomAttribute : MediatorMiddlewareAttribute
{
public string MyProperty { get; set; }
}
public partial class MyHandler : IRequestHandler<MyRequest, MyResponse>
{
[MyCustomAttribute(MyProperty = "Hello")]
public Task<MyResponse> Handle(MyRequest request, CancellationToken ct)
{
// ...
}
}
public class MyMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(
IMediatorContext context,
RequestHandlerDelegate<TResult> next,
CancellationToken cancellationToken
)
{
var attribute = context.GetHandlerAttribute<MyCustomAttribute>();
if (attribute != null)
{
// do something with attribute.MyProperty
}
return await next();
}
}

The Shiny.Mediator.AspNet package maps your request, command, and stream handlers directly to ASP.NET Core minimal-API endpoints — no reflection, no repetitive app.MapPost(...) boilerplate.

  1. Annotate a handler method with [MediatorHttpGet], [MediatorHttpPost], [MediatorHttpPut], or [MediatorHttpDelete], optionally grouping the class with [MediatorHttpGroup].

    [MediatorScoped] // handlers must be scoped in ASP.NET
    [MediatorHttpGroup("/routes", RequiresAuthorization = true)]
    public class MyHandler : IRequestHandler<MyRequest, MyResult>
    {
    [MediatorHttpPost("MyOperation", "/my")] // -> /routes/my
    public async Task<MyResult> Handle(MyRequest request, IMediatorContext context, CancellationToken ct)
    => new MyResult();
    }
  2. Call the generated MapGeneratedMediatorEndpoints() after building your app.

    app.MapGeneratedMediatorEndpoints();

Stream handlers are exposed as Server-Sent Events (GET/POST only). See the full walkthrough, attribute properties, and fluent minimal-API registration on the ASP.NET Core (Handler to Endpoint) page.

The generator can also expose your request and command contracts as AI-callable tools via Microsoft.Extensions.AI. Enable it with the ShinyMediatorGenerateAITools MSBuild property (default off; requires the Microsoft.Extensions.AI package or you’ll get compiler error SHINYMED100). See AI Tools.