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.
- 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 startupservices.AddShinyMediator(x => x.AddMediatorRegistry()); // this method is generated in your assemblyTODO: configuration options
- 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.
- 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;- 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; }}- 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>- 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.
```csharppublic 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(); }}- ASP.NET Endpoint Creation
TODO