- 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","ISerializer","ISerializerService","[Truncated]"]
# 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:
```csharp
[MediatorSingleton] // Stateless handlers
[MediatorScoped] // Handlers needing per-request services (DbContext)
```
**Critical: Partial Class Requirement**
When using **any middleware attribute** (`[Cache]`, `[OfflineAvailable]`, `[Resilient]`, `[MainThread]`, `[TimerRefresh]`, `[Sample]`, `[Throttle]`), the handler class **must be declared as `partial`**:
```csharp
[MediatorSingleton]
public partial class MyHandler : IRequestHandler<MyRequest, MyResult> // partial required!
{
[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:**
```csharp
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
);
app.MapGeneratedMediatorEndpoints();
```
**MAUI:**
```csharp
builder.AddShinyMediator(x => x
.AddMediatorRegistry()
.UseMaui()
.AddMauiPersistentCache()
.PreventEventExceptions()
);
```
**Blazor:**
```csharp
builder.Services.AddShinyMediator(x => x
.AddMediatorRegistry()
.UseBlazor()
.PreventEventExceptions()
);
```
**Logging & Configuration are optional (v6.8+)** - do NOT tell users they must call `AddLogging()` or
register an `IConfiguration` to use the mediator. Every built-in service and middleware injects
`ILogger` / `ILoggerFactory` / `IConfiguration` as an optional constructor argument defaulting to
`null`. With no logging registered, log statements are skipped. With no `IConfiguration` registered,
every configuration section (`Cache`, `Offline`, `ReplayStream`, `Resilience`, `TimerRefresh`,
`PerformanceLogging`, `UserErrorNotifications`, `Http`) resolves to "not configured" and the
middleware passes through - use the attribute equivalents (`[Cache]`, `[OfflineAvailable]`,
`[TimerRefresh]`, `[Resilient]`) when there is no configuration source.
Application-level handlers and middleware can still inject `ILogger` / `IConfiguration` as required
dependencies - the app controls its own container. Follow the optional convention (`ILogger<T>? logger = null`
last in the constructor, `logger?.LogDebug(...)`) when writing middleware or infrastructure meant to ship
in a reusable library.
## JSON Serialization (v6.6+)
Mediator uses `Shiny.ISerializer` from `Shiny.Extensions.Serialization`. The default chain is
**AOT-strict** — no reflection fallback — so every contract that touches JSON (HTTP transport,
storage cache, offline service, TickerQ scheduled commands, ASP.NET endpoints) must be in a
registered `JsonSerializerContext`. Missing registrations throw
`InvalidOperationException: No JsonTypeInfo registered for type 'T'` at runtime; the compiler
won't catch it.
**Default pattern when generating contracts.** Always emit a `[ShinyJsonContext]`-tagged partial
`JsonSerializerContext` alongside the contracts and list every request, response, event, and
scheduled-command type:
```csharp
using System.Text.Json.Serialization;
using Shiny;
[ShinyJsonContext]
[JsonSerializable(typeof(GetCustomerRequest))]
[JsonSerializable(typeof(CustomerResponse))]
[JsonSerializable(typeof(OrderPlacedEvent))]
internal partial class AppJsonContext : JsonSerializerContext;
```
The `[ModuleInitializer]` emitted by the extensions generator registers this context with
`Shiny.Json` before `Main` runs — no `services.AddJsonContext(...)` call needed.
**Opt-in auto-generation (default off).** Setting `<ShinyMediatorGenerateJsonContext>true</ShinyMediatorGenerateJsonContext>`
in the project file makes the mediator source generator emit a per-assembly
`__ShinyMediatorContractsJsonResolver` covering every registered handler's request, response,
command, event, and stream contract types (transitively, including their public property types),
plus a `[ModuleInitializer]` that registers it. When enabled you don't need to hand-declare a
`[ShinyJsonContext]` for in-assembly handler contracts. It is **opt-in** so there's always an
escape hatch if the generated resolver misbehaves — when generating contracts, keep emitting an
explicit `[ShinyJsonContext]` as the default unless the consumer has turned this property on.
Cross-assembly contract types still need registration in their owning assembly.
**Collections (`List<T>`, `T[]`, `IAsyncEnumerable<T>`, etc.) of a contract type.** Mark the
element type with `[ShinyJsonInclude]`:
```csharp
[ShinyJsonInclude]
public partial class Customer { /* ... */ }
```
**OpenAPI HTTP clients with `GenerateJsonConverters="true"`:** the OpenAPI generator emits a
custom `IJsonTypeInfoResolver` covering every generated model + contract + enum
(including `Nullable<TEnum>` / `List<T>` / `T[]` shapes) plus a `[ModuleInitializer]`. Users don't
need `[ShinyJsonContext]` for OpenAPI-generated types.
**Attribute-driven HTTP clients (`[Get]` / `[Post]` / `[Body]`):** user-written types; require
explicit `[ShinyJsonContext]` registration.
**Migration from v5/early-v6:**
- `ISerializerService` was removed — replace with `Shiny.ISerializer` (Shiny namespace, from
`Shiny.Extensions.Serialization`).
- `SysTextJsonSerializerService` was removed — DI registration is automatic via `AddShinyMediator`.
- `ShinyMediatorBuilder.SetSerializer<T>()` was removed — replace by either registering a different
`Shiny.ISerializer` in DI before `AddShinyMediator`, or calling `Shiny.Json.AddContext` /
`Shiny.Json.AddResolver`.
- The legacy `[SourceGenerateJsonConverter]` attribute still works for backward compatibility but
new code should use `[ShinyJsonContext]` + `[JsonSerializable]` instead — it gives first-class
AOT coverage of collection shapes.
**Tests / development scenarios that need reflection fallback** (ad-hoc / anonymous types):
```csharp
// In an [assembly: ...] or a [ModuleInitializer]
Shiny.Json.AddResolver(new System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver());
```
This is not AOT-safe — use only in test fixtures or non-production code.
## Code Generation Instructions
When generating Shiny Mediator code:
### 1. Contracts
Always use records for immutability:
```csharp
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:
```csharp
[MediatorScoped]
public class GetUserRequestHandler : IRequestHandler<GetUserRequest, UserDto>
{
public Task<UserDto> Handle(
GetUserRequest request,
IMediatorContext context,
CancellationToken cancellationToken)
{
// Implementation
}
}
```
### 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`:**
```csharp
[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.
```csharp
[MiddlewareOrder(-100)] // Runs before middleware with higher order values
[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:**
```csharp
var response = await mediator.Request(new GetUserRequest(1));
var user = response.Result;
```
**Command:**
```csharp
await mediator.Send(new CreateUserCommand("John", "john@example.com"));
```
**Event:**
```csharp
await mediator.Publish(new UserCreatedEvent(1, "John"));
```
**Chaining via Context:**
```csharp
public async Task<UserDto> Handle(GetUserRequest request, IMediatorContext context, CancellationToken ct)
{
// Use context to chain operations (shares scope)
await context.Publish(new UserAccessedEvent(request.UserId));
return new UserDto(...);
}
```
### Event Subscriptions & Streaming
**WaitForSingleEvent** - Await a single event occurrence (with optional filter):
```csharp
// Wait for a specific event (blocks until event fires or cancellation)
var evt = await mediator.WaitForSingleEvent<OrderCompletedEvent>(
filter: e => e.OrderId == orderId,
cancellationToken: ct
);
```
**EventStream** - Continuous IAsyncEnumerable stream of events (uses Channels internally):
```csharp
// Consume events as an async stream
await foreach (var evt in mediator.EventStream<PriceUpdatedEvent>(cancellationToken: ct))
{
Console.WriteLine($"New price: {evt.Price}");
}
```
**Subscribe** - Manual subscription returning IDisposable:
```csharp
var sub = mediator.Subscribe<MyEvent>((ev, ctx, ct) =>
{
Console.WriteLine($"Event received: {ev}");
return Task.CompletedTask;
});
// Later: sub.Dispose() to unsubscribe
```
### 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:**
```csharp
app.MapGet("/events", ([FromServices] IMediator mediator) =>
TypedResults.ServerSentEvents(mediator.EventStream<MyEvent>())
);
```
**Stream handler as auto-generated SSE endpoint:**
```csharp
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.
```csharp
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:
```csharp
[Get("/api/orders/{OrderId}")]
public class GetOrderRequest : IRequest<OrderDto>
{
public int OrderId { get; set; } // Route parameter (matches {OrderId})
[Query("status")]
public string? Status { get; set; } // ?status=value
[Header("Authorization")]
public string? AuthToken { get; set; } // HTTP header
}
[Post("/api/orders")]
public class CreateOrderRequest : IRequest<OrderDto>
{
[Body]
public CreateOrderBody? Body { get; set; } // JSON request body
}
```
The source generator creates handler classes inheriting `BaseHttpRequestHandler` that build routes, add query/header parameters, serialize bodies, and call `IHttpClientFactory`.
عرض على GitHub