基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/shinyorg/skills --skill shiny-mediator命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
Generate .NET MAUI Shell pages, ViewModels, navigation, and source-generated routes using Shiny MAUI Shell
iBeacon and Eddystone ranging, background region monitoring, and broadcasting for .NET MAUI, iOS, Android, macOS, Windows, Linux and Blazor using Shiny.Beacons
Generate code using Shiny.BluetoothLE.Hosting, a BLE peripheral hosting library for .NET with GATT server, advertising, and L2CAP CoC channels
| 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]"] |
You are an expert in Shiny Mediator, a mediator pattern library for .NET applications.
Invoke this skill when the user wants to:
Documentation: https://shinylib.net/mediator
Shiny Mediator is AOT & trimming friendly, using source generators for automatic DI registration.
| 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 |
Always use registration attributes:
[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:
[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.
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()
);
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.
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:
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]:
[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.[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):
// 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.
When generating Shiny Mediator code:
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;
Include all three parameters in Handle method:
[MediatorScoped]
public class GetUserRequestHandler : IRequestHandler<GetUserRequest, UserDto>
{
public Task<UserDto> Handle(
GetUserRequest request,
IMediatorContext context,
CancellationToken cancellationToken)
{
// Implementation
}
}
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 validationWhen 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(...) { }
}
Use [MiddlewareOrder(int)] on custom middleware classes to control execution order. Lower values run first (outermost). Default is 0.
[MiddlewareOrder(-100)] // Runs before middleware with higher order values
[MediatorSingleton]
public class EarlyMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult>
where TRequest : IRequest<TResult>
{ ... }
Place files in appropriate folders:
Contracts/{Name}Request.cs, Contracts/{Name}Command.csHandlers/{Name}Handler.csMiddleware/{Name}Middleware.csRequest:
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)
{
// Use context to chain operations (shares scope)
await context.Publish(new UserAccessedEvent(request.UserId));
return new UserDto(...);
}
WaitForSingleEvent - Await a single event occurrence (with optional filter):
// 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):
// 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:
var sub = mediator.Subscribe<MyEvent>((ev, ctx, ct) =>
{
Console.WriteLine($"Event received: {ev}");
return Task.CompletedTask;
});
// Later: sub.Dispose() to unsubscribe
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;
Shiny Mediator generates strongly-typed HTTP client handlers from contract classes decorated with HTTP method attributes. No manual HttpClient code needed.
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; } // 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.