| name | shiny-mediator |
| description | Generate Shiny Mediator handlers, contracts, middleware, and scaffold projects for .NET applications |
| auto_invoke | true |
| triggers | ["mediator","handler","request handler","command handler","event handler","stream handler","middleware","IRequest","ICommand","IEvent","CQRS","Shiny.Mediator","server sent events","SSE","EventStream","WaitForSingleEvent","IAsyncEnumerable","IStreamRequest","IServerSentEventsStream","OpenAPI","HTTP client","MediatorHttp","swagger","contract-first","strongly typed HTTP","AI tools","AITool","Microsoft.Extensions.AI","AddGeneratedAITools","ShinyMediatorGenerateAITools"] |
Shiny Mediator Skill
You are an expert in Shiny Mediator, a mediator pattern library for .NET applications.
When to Use This Skill
Invoke this skill when the user wants to:
- Create request handlers, command handlers, event handlers, or stream handlers
- Generate contracts (IRequest, ICommand, IEvent, IStreamRequest)
- Add middleware (caching, resilience, validation, offline)
- Scaffold ASP.NET, MAUI, or Blazor projects with Shiny Mediator
- Configure Shiny Mediator in their application
- Set up ASP.NET Server-Sent Events (SSE) endpoints with stream handlers
- Use event subscriptions (WaitForSingleEvent, EventStream, Subscribe)
- Generate strongly-typed HTTP clients from OpenAPI/Swagger specs
- Create contract-first HTTP request handlers with [Get], [Post], etc.
- Expose mediator contracts as AI tools via Microsoft.Extensions.AI
- Migrate from MediatR to Shiny Mediator
Library Overview
Documentation: https://shinylib.net/mediator
Shiny Mediator is AOT & trimming friendly, using source generators for automatic DI registration.
Core Patterns
| Pattern | Contract | Handler | Usage |
|---|
| Request | IRequest<TResult> | IRequestHandler<TRequest, TResult> | Queries returning data |
| Command | ICommand | ICommandHandler<TCommand> | Void state changes |
| Event | IEvent | IEventHandler<TEvent> | Pub/sub notifications |
| Stream | IStreamRequest<TResult> | IStreamRequestHandler<TRequest, TResult> | IAsyncEnumerable |
Handler Registration
Always use registration attributes:
[MediatorSingleton]
[MediatorScoped]
Critical: Partial Class Requirement
When using any middleware attribute ([Cache], [OfflineAvailable], [Resilient], [MainThread], [TimerRefresh], [Sample], [Throttle]), the handler class must be declared as partial:
[MediatorSingleton]
public partial class MyHandler : IRequestHandler<MyRequest, MyResult>
{
[Cache(AbsoluteExpirationSeconds = 60)]
public Task<MyResult> Handle(...) { }
}
This enables the source generator to create the IHandlerAttributeMarker implementation. Without partial, you'll get error SHINY001.
Basic Setup
ASP.NET:
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
);
app.MapGeneratedMediatorEndpoints();
MAUI:
builder.AddShinyMediator(x => x
.AddMediatorRegistry()
.UseMaui()
.AddMauiPersistentCache()
.PreventEventExceptions()
);
Blazor:
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
.UseBlazor()
.PreventEventExceptions()
);
Code Generation Instructions
When generating Shiny Mediator code:
1. Contracts
Always use records for immutability:
public record GetUserRequest(int UserId) : IRequest<UserDto>;
public record CreateUserCommand(string Name, string Email) : ICommand;
public record UserCreatedEvent(int UserId, string Name) : IEvent;
2. Handlers
Include all three parameters in Handle method:
[MediatorScoped]
public class GetUserRequestHandler : IRequestHandler<GetUserRequest, UserDto>
{
public Task<UserDto> Handle(
GetUserRequest request,
IMediatorContext context,
CancellationToken cancellationToken)
{
}
}
3. Middleware Attributes
Apply to handler methods as needed:
[Cache(AbsoluteExpirationSeconds = N)] - Cacheable queries
[OfflineAvailable] - Offline storage for mobile
[Resilient("policyName")] - Retry/timeout policies
[MainThread] - MAUI main thread execution
[TimerRefresh(milliseconds)] - Auto-refresh streams
[Sample(milliseconds)] - Fixed-window sampling (last event in window executes)
[Throttle(milliseconds)] - True throttle (first event executes, cooldown discards rest)
[Validate] - Data annotation validation
When using ANY of these attributes, the handler class MUST be partial:
[MediatorSingleton]
public partial class CachedHandler : IRequestHandler<MyRequest, MyData>
{
[Cache(AbsoluteExpirationSeconds = 60)]
[OfflineAvailable]
public Task<MyData> Handle(...) { }
}
4. Middleware Ordering
Use [MiddlewareOrder(int)] on custom middleware classes to control execution order. Lower values run first (outermost). Default is 0.
[MiddlewareOrder(-100)]
[MediatorSingleton]
public class EarlyMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{ ... }
5. File Organization
Place files in appropriate folders:
- Contracts:
Contracts/{Name}Request.cs, Contracts/{Name}Command.cs
- Handlers:
Handlers/{Name}Handler.cs
- Middleware:
Middleware/{Name}Middleware.cs
Usage Examples
Request:
var response = await mediator.Request(new GetUserRequest(1));
var user = response.Result;
Command:
await mediator.Send(new CreateUserCommand("John", "john@example.com"));
Event:
await mediator.Publish(new UserCreatedEvent(1, "John"));
Chaining via Context:
public async Task<UserDto> Handle(GetUserRequest request, IMediatorContext context, CancellationToken ct)
{
await context.Publish(new UserAccessedEvent(request.UserId));
return new UserDto(...);
}
Event Subscriptions & Streaming
WaitForSingleEvent - Await a single event occurrence (with optional filter):
var evt = await mediator.WaitForSingleEvent<OrderCompletedEvent>(
filter: e => e.OrderId == orderId,
cancellationToken: ct
);
EventStream - Continuous IAsyncEnumerable stream of events (uses Channels internally):
await foreach (var evt in mediator.EventStream<PriceUpdatedEvent>(cancellationToken: ct))
{
Console.WriteLine($"New price: {evt.Price}");
}
Subscribe - Manual subscription returning IDisposable:
var sub = mediator.Subscribe<MyEvent>((ev, ctx, ct) =>
{
Console.WriteLine($"Event received: {ev}");
return Task.CompletedTask;
});
ASP.NET Server-Sent Events (SSE)
Stream handlers decorated with [MediatorHttpGet] or [MediatorHttpPost] on an IStreamRequestHandler are automatically generated as SSE endpoints by the source generator via MapGeneratedMediatorEndpoints().
Manual SSE endpoint with EventStream:
app.MapGet("/events", ([FromServices] IMediator mediator) =>
TypedResults.ServerSentEvents(mediator.EventStream<MyEvent>())
);
Stream handler as auto-generated SSE endpoint:
public record TickerStreamRequest : IStreamRequest<int>;
[MediatorScoped]
public class TickerStreamHandler : IStreamRequestHandler<TickerStreamRequest, int>
{
[MediatorHttpGet("/ticker")]
public async IAsyncEnumerable<int> Handle(
TickerStreamRequest request,
IMediatorContext context,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var i = 0;
while (!cancellationToken.IsCancellationRequested)
{
yield return i++;
await Task.Delay(1000, cancellationToken);
}
}
}
HTTP client-side SSE consumption: Implement IServerSentEventsStream marker on the contract to indicate the server returns SSE format. The generated HTTP handler will use ReadServerSentEvents<T>() to parse the data: prefixed SSE lines.
public record TickerStreamRequest : IStreamRequest<int>, IServerSentEventsStream;
Contract-First HTTP Clients
Shiny Mediator generates strongly-typed HTTP client handlers from contract classes decorated with HTTP method attributes. No manual HttpClient code needed.
Manual HTTP Contracts
Decorate request classes with [Get], [Post], [Put], [Delete], [Patch] and use [Query], [Header], [Body] on properties:
[Get("/api/orders/{OrderId}")]
public class GetOrderRequest : IRequest<OrderDto>
{
public int OrderId { get; set; }
[Query("status")]
public string? Status { get; set; }
[Header("Authorization")]
public string? AuthToken { get; set; }
}
[Post("/api/orders")]
public class CreateOrderRequest : IRequest<OrderDto>
{
[Body]
public CreateOrderBody? Body { get; set; }
}
The source generator creates handler classes inheriting BaseHttpRequestHandler that build routes, add query/header parameters, serialize bodies, and call IHttpClientFactory.
Registration:
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
.AddStrongTypedHttpClient()
);
OpenAPI Client Generation
Generate contracts, models, and handlers directly from OpenAPI/Swagger specs. Add a <MediatorHttp> item in your .csproj:
<ItemGroup>
<MediatorHttp Include="MyApi"
Uri="https://api.example.com/swagger/v1/swagger.json"
Namespace="MyApp.ExternalApi"
ContractPostfix="HttpRequest"
GenerateJsonConverters="true"
Visible="false" />
<MediatorHttp Include="./specs/openapi.yaml"
Namespace="MyApp.LocalApi"
Visible="false" />
</ItemGroup>
Include can be a file path (for local specs) or a logical name when Uri is set separately.
MediatorHttp metadata options:
| Metadata | Description |
|---|
Uri | URL or local path to OpenAPI JSON/YAML spec |
Namespace | C# namespace for generated types |
ContractPrefix | Prefix for generated contract class names |
ContractPostfix | Postfix for generated contract class names |
UseInternalClasses | Generate internal classes instead of public |
GenerateModelsOnly | Only generate models, no handlers |
GenerateJsonConverters | Generate JsonConverter implementations for enums |
Registration of generated OpenAPI client:
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
.AddGeneratedOpenApiClient()
);
Then use like any other mediator request:
var result = await mediator.Request(new GetPetsHttpRequest { Status = "available" });
AI Tools Integration (Microsoft.Extensions.AI)
Shiny Mediator can expose your request and command contracts as AI-callable tools via Microsoft.Extensions.AI. The source generator discovers contracts annotated with [Description] and generates AIFunction wrappers with JSON schemas automatically.
Prerequisites
- Install the
Microsoft.Extensions.AI NuGet package:
dotnet add package Microsoft.Extensions.AI
- Enable AI tool generation in your
.csproj:
<PropertyGroup>
<ShinyMediatorGenerateAITools>true</ShinyMediatorGenerateAITools>
</PropertyGroup>
:::caution
If ShinyMediatorGenerateAITools is enabled but Microsoft.Extensions.AI is not referenced, the source generator emits compiler error SHINYMED100.
:::
Annotating Contracts
Add [Description] to your contracts and their properties to make them discoverable by AI:
using System.ComponentModel;
[Description("Gets the weather forecast for a given city")]
public record GetWeatherRequest(
[property: Description("The city name to get weather for")]
string City
) : IRequest<WeatherResult>;
[Description("Performs a mathematical calculation")]
public record CalculateRequest(
[property: Description("First operand")]
double A,
[property: Description("Math operator: +, -, *, /")]
string Operator,
[property: Description("Second operand")]
double B
) : IRequest<double>;
[Description("Sends a notification to a user")]
public record SendNotificationCommand(
[property: Description("The user ID to notify")]
int UserId,
[property: Description("The notification message")]
string Message
) : ICommand;
Registration
Register the generated AI tools with the mediator builder:
builder.Services.AddShinyMediator(cfg =>
{
cfg.AddMediatorRegistry();
cfg.AddGeneratedAITools();
});
Using with a Chat Client
Retrieve the tools from DI and pass them to any IChatClient:
using Microsoft.Extensions.AI;
var tools = host.Services.GetServices<AITool>().ToList();
var options = new ChatOptions { Tools = tools };
var response = await chatClient.GetResponseAsync(history, options);
What Gets Generated
For each contract with [Description] that implements IRequest<T> or ICommand, the source generator produces:
- An
AIFunction class — wraps the mediator call with a JSON schema built from the contract's properties
- A registration extension method —
AddGeneratedAITools() on ShinyMediatorBuilder that registers all generated tools as AITool singletons
The generated JSON schema includes property types, descriptions, required fields, enum values, and default values.
Supported Property Types
| Type | JSON Schema Type |
|---|
string, Guid, Uri, DateTime, DateTimeOffset, DateOnly | string |
bool | boolean |
int, long, short, byte | integer |
float, double, decimal | number |
| Enums | string with enum values |
Arrays / IEnumerable<T> | array |
| Complex types | object |
Best Practices
- Use records for contracts - Immutable, value equality
- One handler per file - Focused and testable
- Appropriate lifetime - Singleton for stateless, Scoped for DbContext
- Use
partial with middleware attributes - Required for [Cache], [OfflineAvailable], [Resilient], etc.
- Chain via context - Use
context.Request() not injecting IMediator
- Implement IContractKey - For custom cache/offline keys
- Always pass CancellationToken - Respect cancellation
- Use IServerSentEventsStream marker - On stream contracts consumed via HTTP SSE
- Stream handlers only support GET/POST - Other HTTP methods are not valid for SSE endpoints
- Use EventStream for SSE push endpoints - Combine
mediator.EventStream<T>() with TypedResults.ServerSentEvents() for event-driven SSE
Reference Files
For detailed templates and examples, see:
reference/templates.md - Code generation templates (includes SSE endpoint templates)
reference/scaffolding.md - Project structure templates
reference/middleware.md - Middleware configuration
reference/api-reference.md - Full API, event subscriptions, SSE, and NuGet packages
Common Packages
dotnet add package Shiny.Mediator
dotnet add package Shiny.Mediator.Maui
dotnet add package Shiny.Mediator.Blazor
dotnet add package Shiny.Mediator.AspNet
dotnet add package Shiny.Mediator.Resilience
dotnet add package Shiny.Mediator.Caching.MicrosoftMemoryCache
dotnet add package Shiny.Mediator.AppSupport
dotnet add package Microsoft.Extensions.AI