Source Generation
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.
Handler & Middleware Registration
Section titled “Handler & Middleware Registration”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 assemblyservices.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());// orbuilder.Services.AddMediatorRegistry();The only thing these attributes decide is the DI lifetime (singleton vs scoped) — they take no constructor arguments or named properties.
Configuration options
Section titled “Configuration options”The attributes themselves aren’t configurable, but the generated registry can be tuned from your
.csproj:
| Property | Default | Description |
|---|---|---|
ShinyMediatorRegistryMethodName | AddMediatorRegistry | Renames the generated registration method. |
ShinyMediatorRegistryUseInternalClass | false | Emits the generated registry class as internal instead of public. |
RootNamespace | assembly name | Namespace the generated code is placed in. |
<PropertyGroup> <ShinyMediatorRegistryMethodName>AddMyAppHandlers</ShinyMediatorRegistryMethodName> <ShinyMediatorRegistryUseInternalClass>true</ShinyMediatorRegistryUseInternalClass></PropertyGroup>Executers (Request & Stream Request)
Section titled “Executers (Request & Stream Request)”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:
| Property | Default |
|---|---|
ShinyMediatorRequestExecutorClassName | GeneratedMediatorRequestExecutor |
ShinyMediatorStreamRequestExecutorClassName | GeneratedMediatorStreamRequestExecutor |
JSON Serialization
Section titled “JSON Serialization”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.
Register your contracts
Section titled “Register your contracts”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;Auto-generate the wiring (opt-in)
Section titled “Auto-generate the wiring (opt-in)”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>Collections
Section titled “Collections”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 { /* ... */ }HTTP client contracts
Section titled “HTTP client contracts”- OpenAPI clients — when
GenerateJsonConverters="true"on aMediatorHttpitem, the generator emits a customIJsonTypeInfoResolverwiring every generated model, contract, and enum (includingNullable<TEnum>andList<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.
Contract Keys
Section titled “Contract Keys”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")}";}[ContractKey("MyRequest_{Name}_{Date:yyyyMMddHHmmss}")]public partial class MyRequest : IRequest<Something>{ public string Name { get; set; } public DateTimeOffset? Date { get; set; }}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.
HTTP Contracts
Section titled “HTTP Contracts”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 clientbuilder.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.
Attribute Usage (handler methods)
Section titled “Attribute Usage (handler methods)”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) { // ... }}Implementing your own handler attributes
Section titled “Implementing your own handler attributes”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(); }}ASP.NET Endpoint Creation
Section titled “ASP.NET Endpoint Creation”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.
-
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/mypublic async Task<MyResult> Handle(MyRequest request, IMediatorContext context, CancellationToken ct)=> new MyResult();} -
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.
AI Tools
Section titled “AI Tools”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.