Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

Source Generation

We have an assortment of source generation features to help reduce boilerplate, make it safe for AOT/trimming scenarios, and ultimately improve performance.

  1. Handler & Middleware Registration

These two attributes [MediatorSingletonAttribute] and [MediatorScopedAttribute] can be applied to your handler and middleware implementations to have source generators create the necessary registration code for you.

They actually do the leg work for #2 below as well, so if you use these attributes you do not need to also use the executer source generators. This saves you having to manually register each handler.

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

TODO: configuration options

  1. Executers (Request & Stream Request)

This is actually to get around a bit of a generics issue within .NET. Our v4 and earlier implementations used reflection to get around the fact that you cannot create a generic type at runtime without knowing the type parameters at compile time. This is solved in v5+ with source generation.

This issue is much easier solved (and performant) when you know the types at compile time. So we provide source generators to create executers for your requests and stream requests.

All source generated registrations will register your implementations as singleton against ALL interfaces they implement.

  1. 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 — it contains no reflection fallback, so every type mediator touches (request, response, event, scheduled-command payload, storage / cache entry) must have a JsonTypeInfo registered. The compiler can’t catch missing registrations; you’ll see InvalidOperationException: No JsonTypeInfo registered for type 'T' at runtime if you forget.

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;

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 collection-shape wrappers automatically.

[ShinyJsonInclude]
public partial class Customer { /* ... */ }

OpenAPI HTTP clients. When GenerateJsonConverters="true" on a MediatorHttp item, the generator emits a custom IJsonTypeInfoResolver that wires every generated model, contract, and enum (including Nullable<TEnum> and List<T> / T[] shapes) via JsonMetadataServices factories plus a [ModuleInitializer]. No [ShinyJsonContext] needed — it’s all automatic.

Attribute-driven HTTP clients ([Get], [Post], [Body]). Request and result types are user-written, so they must be in a registered [ShinyJsonContext] partial.

Replacing [SourceGenerateJsonConverter] (legacy v5/early-v6 attribute): still works for the HTTP client paths and remains in the source generator for backward compatibility, but new code should prefer [ShinyJsonContext] + [JsonSerializable] for first-class AOT coverage of collection shapes.

public class MyRequest : IRequest<MyResponse>
{
public string Name { get; set; }
}
public class MyResponse
{
public string Message { get; set; }
}
[ShinyJsonContext]
[JsonSerializable(typeof(MyRequest))]
[JsonSerializable(typeof(MyResponse))]
internal partial class AppJsonContext : JsonSerializerContext;
  1. Contract Keys

Contract key source generation is more of a convenience feature. Creating request keys on your contracts can be tedious. You also had to check for things like nulls, etc.

BEFORE

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

AFTER

[ContractKey("MyRequest_{Name}_{Date:yyyyMMddHHmmss}")]
public class MyRequest : IRequest<Something>
{
public string Name { get; set;}
public DateTimeOffset? Date { get; set; }
}
  1. HTTP Contracts

There are tons of libraries out there to do this already. Refitter is a great option. However, if you want to keep everything within Shiny Mediator because it allows our users to bring all of the middleware goodness to their HTTP calls.

<ItemGroup>
<MediatorHttp Include="" />
<MediatorHttp Include="" />
</ItemGroup>
  1. Attribute Usage

This may sound weird, but attributes on methods required a level of deep reflection to get at. With our war on reflection for the benefit of AOT and trimming, we created source generators to help with this.

public partial class MyHandler : IRequestHandler<MyRequest, MyResponse>
{
[MyCustomAttribute]
public Task<MyResponse> Handle(MyRequest request, CancellationToken ct)
{
...
}
}
### Implementing Your Own Handler Attributes
Best of all, our source generator can work with your own mediator attributes. Simply inherit `Shiny.Mediator.MediatorMiddlewareAttribute` and we'll pick it off the handlers for you.
```csharp
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
}
await next();
}
}
  1. ASP.NET Endpoint Creation

TODO