Mediator Releases
6.6.0 TBD
Section titled “6.6.0 TBD”ISerializerService interface removed. Mediator now consumes Shiny.ISerializer from the new Shiny.Extensions.Serialization package (registered automatically by AddShinyMediator). Replace any direct ISerializerService injection with Shiny.ISerializer. The runtime-typed Deserialize(string, Type) overload moved to the Shiny.Mediator.Infrastructure.SerializerRuntimeExtensions extension class.SysTextJsonSerializerService class removed. No replacement — the default ISerializer is Shiny.Json.Default, which is wired into DI by AddShinyMediator. Tests that need ad-hoc / anonymous-type serialization can construct a Shiny.Impl.DefaultJsonSerializer and add DefaultJsonTypeInfoResolver to its chain.ShinyMediatorBuilder.SetSerializer<T>() removed. Customize serialization by registering a different Shiny.ISerializer in DI before AddShinyMediator, or by calling Shiny.Json.AddContext / Shiny.Json.AddResolver to extend the shared chain.HttpHandlerServices.Serializer type changed from ISerializerService to Shiny.ISerializer. Affects subclasses and direct constructions of the record.JsonSerializerContext, any request/response/event/scheduled-command that hits the serializer throws InvalidOperationException: No JsonTypeInfo registered for type 'T'. Register your types via [ShinyJsonContext] partial class AppContext : JsonSerializerContext with [JsonSerializable(typeof(T))] entries — module-init auto-registers it with Shiny.Json. See the new JSON Serialization section.[MediatorSingleton] / [MediatorScoped]-discovered handler, transitively collects request, response, command, event, stream-request element types plus their public property types (so RideTime → Position and the like get pulled in automatically), emits a per-type JsonConverter<T> for each in-assembly contract type, and ships a single __ShinyMediatorContractsJsonResolver per assembly that wires them via JsonMetadataServices.CreateValueInfo plus collection wrappers (List<T> / T[] / IEnumerable<T> / IList<T> / ICollection<T> / IReadOnlyList<T> / IReadOnlyCollection<T> / IAsyncEnumerable<T>). A [ModuleInitializer] registers the resolver with Shiny.Json before Main. Default on — opt out with <ShinyMediatorGenerateJsonContext>false</ShinyMediatorGenerateJsonContext>. Eliminates the manual [ShinyJsonContext] / [JsonSerializable] declaration for the common handler-flow path; cross-assembly types (e.g. types from third-party NuGets) still need explicit registration.GenerateJsonConverters="true" on a MediatorHttp item, the generator emits a custom IJsonTypeInfoResolver per client that wires every generated model, contract, and enum (including Nullable<TEnum> and List<T> / T[] shapes) via JsonMetadataServices factories, plus a [ModuleInitializer] that registers it with Shiny.Json. Zero consumer setup required.StorageCacheService refactored to a non-generic envelope. InternalCacheEntry<T> → InternalCacheEntry carrying a pre-serialized string ValueJson. The persisted shape no longer depends on the closed-generic type of the cached value, eliminating the failure mode where caching IReadOnlyList<ParkRideInfo> threw No JsonTypeInfo registered for type 'InternalCacheEntry<IReadOnlyList<ParkRideInfo>>' under the strict chain. StorageCacheService now takes Shiny.ISerializer alongside its existing dependencies.Shiny.Mediator.Infrastructure.AppSupportJsonResolver (in Shiny.Mediator.AppSupport) wires JsonTypeInfo for InternalCacheEntry, OfflineStore, CacheItemConfig, and the AbstractFileStorageService (category → key → filename) index via hand-rolled converters. A [ModuleInitializer] registers it with Shiny.Json — users never have to declare these envelope types in their own JsonSerializerContext.ValidateResult. A new Shiny.Mediator.Infrastructure.AspNetJsonResolver (in Shiny.Mediator.AspNet) wires JsonTypeInfo for ValidateResult. ValidationJsonExceptionHandler no longer calls raw JsonSerializer.Serialize (which silently bypassed the chain and was AOT-hostile) — it now routes through the injected Shiny.ISerializer so the 400-response body works under the strict chain without users declaring ValidateResult anywhere.SerializeToElement. Generated AIFunction.InvokeCoreAsync previously called JsonSerializer.SerializeToElement(arguments) (reflection-based, AOT-hostile) to extract typed values from the MEAI AIFunctionArguments dictionary. It now reads each property directly via a switch covering the three runtime shapes — already-typed boxed primitive, JsonElement, or null — with explicit numeric widening (e.g. long → int) for STJ’s number defaults. Complex / collection property types deserialize through Shiny.Json.Default.Options so the chain (which the new contracts resolver populates) supplies the JsonTypeInfo.MemoryCacheService no longer uses reflection over MemoryCache internals. Remove(partialMatch: true) and Clear now use an in-process key index kept in sync via PostEvictionCallback, making the cache AOT/trim-safe and changing prefix removal from O(all entries) to O(matches).JsonConverterSourceGenerator.GenerateJsonConverter(context, typeSymbol, attachAttribute: false) overload. Emits just the per-type JsonConverter<T> class without trying to wire a [JsonConverter] attribute via a partial declaration — so the contracts resolver can generate converters for non-partial user records without forcing partial everywhere. The legacy [SourceGenerateJsonConverter] path keeps attachAttribute: true semantics unchanged.MemoryCacheExtensions.RemoveByKeyStartsWith actually filters by prefix. Previous implementation was missing the StartsWith predicate and silently removed every entry in the cache.Shiny.Extensions.DependencyInjection to v5 and added Shiny.Extensions.Serialization v5.6.5.0 TBD
Section titled “6.5.0 TBD”MediatorHttp items use GenerateJsonConverters="true", the JsonConverterSourceGenerator previously detected only TypeKind.Enum and missed Nullable<TEnum> (which has TypeKind.Struct). Nullable enum properties fell through to JsonSerializer.Deserialize<TEnum?> and JsonSerializer.Serialize. At runtime under NativeAOT this triggers NullableConverter<TEnum>, which uses reflection to look up the inner enum’s converter and throws “missing native code or metadata” against the nullable converter type. The generator now unwraps Nullable<TEnum> and emits (TEnum?)Enum.Parse<TEnum>(reader.GetString()!) on read and value.Prop?.ToString() on write — fully inline and trim/AOT-safe6.4.0 - May 17, 2026
Section titled “6.4.0 - May 17, 2026”StorageService.DeleteFile — inverted condition (!File.Exists) prevented cached files from ever being deletedLocalEventExecutor — each event handler was creating a new scope instead of reusing the parent scope, leading to accumulated unreleased scopesMediatorContext — Request, Send, and Publish now properly dispose child scopes in a finally blockStorageCacheService concurrency — replaced the single global semaphore with per-key locking via the new KeyedLocker, so the factory runs exactly once per key without serializing unrelated keys. Get now also takes the per-key lock, eliminating a sliding-expiration race where two concurrent reads could overwrite each otherMemoryCacheService.GetOrCreate cache stampede — IMemoryCache.GetOrCreateAsync does not lock the factory, so N concurrent callers for the same key would each invoke the factory. Now wraps the factory in a per-key KeyedLocker so it runs exactly once per keyHttpRequestCacheMiddleware double-wrap bug — TryCacheEntry was wrapping the result in a CacheEntry<TResult> before calling ICacheService.Set<TResult>(...), which itself wraps the value, so the cache stored CacheEntry<CacheEntry<TResult>> and subsequent Get<TResult> calls always missedHttpRequestCacheMiddleware cache stampede — Get → next → Set was unsynchronized, so concurrent requests for the same URL would all hit the network. Now per-key locked via KeyedLockerShiny.Mediator.Infrastructure.KeyedLocker — per-key async mutual exclusion primitive used by cache services and middleware for stampede protection; safe for any singleton component that needs to serialize work by keyPublishToBackground exception handling — TryHandle was fire-and-forget (_ =), so exception handlers were never awaited. Now properly awaitedInMemoryCommandScheduler scope and activity lifecycle — moved scope/activity creation outside the try block and added finally disposal to prevent leaksInternetService.WaitForAvailable — single TaskCompletionSource meant only one concurrent caller could wait. Now supports multiple concurrent waitersMemoryCacheService.Remove with partial match — was modifying the cache entry collection during enumeration, causing InvalidOperationException. Now snapshots matching keys before removalQueuedEventMiddleware SampleState timer — added CancellationTokenSource to properly cancel in-flight timers on dispose and prevent stale callbacksAbstractFileStorageService concurrent access — simplified semaphore patterns and prevented potential deadlocks from double semaphore acquisition in GetFileIndexer. Data-file writes are now serialized per (category, key) with KeyedLocker, so two concurrent Set calls on the same key can no longer tear the file contentEventStream extension method (unused TaskCompletionSource and cancellation registration)6.3.0 - April 2026
Section titled “6.3.0 - April 2026”Microsoft.Extensions.AI. Annotate contracts with [Description] and the source generator produces AIFunction wrappers with JSON schemas automatically. Enable with ShinyMediatorGenerateAITools MSBuild property.Shiny.Mediator.SourceGenerators — no separate Shiny.Mediator.MicrosoftAI.SourceGenerators package neededSHINYMED100 is emitted when ShinyMediatorGenerateAITools is enabled but Microsoft.Extensions.AI is not referenced[Sample] and [Throttle] event middleware is now registered automatically as part of the default middleware — no need to call a separate registration methodReplayStreamMiddleware, SentryStreamRequestMiddleware, TimerRefreshStreamRequestMiddleware) — enumerators are now properly disposed in a finally block to prevent resource leaksMauiEventCollector — page tracking collection is now synchronized with a lock to prevent concurrent modification issuesMediator.Request — scope is now properly disposed after request completionPublishToBackground scope lifetime — service scope and activity were being disposed before the background task completed, now properly managed within the background taskInMemoryCommandScheduler race condition — timer creation is now inside the lock to prevent duplicate timersConnectivityBroadcaster and MauiEventCollector to resolve IApplication from the service provider instead of constructor injection to avoid circular dependency issues6.2.1 - February 26, 2026
Section titled “6.2.1 - February 26, 2026”[Sample] attribute for event handler methods - fixed-window sampling where the first event starts a timer, subsequent events replace the pending delegate without resetting the timer, and the last event is executed when the window expires[Throttle] now implements true throttle behavior - the first event executes immediately, then all subsequent events within the cooldown window are discarded. Previously, [Throttle] implemented debounce/sample behavior// Sample: fixed-window, last event wins at end of window// No explicit registration needed — included in default middleware
public partial class MyEventHandler : IEventHandler<MyEvent>{ [Sample(1000)] // 1 second window public Task Handle(MyEvent myEvent, IMediatorContext context, CancellationToken ct) { // First event starts a 1s timer. Subsequent events replace the pending delegate // but do NOT reset the timer. When the timer fires, the last event is executed. }}
// Throttle: first event executes immediately, cooldown discards the rest// No explicit registration needed — included in default middleware
public partial class MyEventHandler : IEventHandler<MyEvent>{ [Throttle(1000)] // 1 second cooldown public Task Handle(MyEvent myEvent, IMediatorContext context, CancellationToken ct) { // First event executes immediately. All events within the next 1s are discarded. // After cooldown expires, the next event executes immediately again. }}6.2.0 - February 19, 2026
Section titled “6.2.0 - February 19, 2026”[MiddlewareOrder(int)] (higher numbers are closer to the handler)services.AddShinyMediator(x => x.AddThrottledEventHandlers());
public class MyEventHandler : IEventHandler<MyEvent>{ [Throttle(1000)] // respresents milliseconds, so 1000 = 1 second public Task Handle(MyEvent myEvent, EventContext context) { // this will ensure that if multiple MyEvent are published within 5 seconds, only the last one will be delivered after the 5 seconds is up // if only one event is published, it will be delivered immediately // if multiple events are published, the timer resets with each new event and only the last event is delivered after 5 seconds of no new events }}v6.1.3 - February 7, 2026
Section titled “v6.1.3 - February 7, 2026”v6.1.2 - February 6, 2026
Section titled “v6.1.2 - February 6, 2026”v6.1.1 - January 16, 2026
Section titled “v6.1.1 - January 16, 2026”v6.1.0 - TBD
Section titled “v6.1.0 - TBD”v6.0.2 - January 3, 2026
Section titled “v6.0.2 - January 3, 2026”v6.0.1 - December 21, 2025
Section titled “v6.0.1 - December 21, 2025”v6.0.0 - December 5, 2025
Section titled “v6.0.0 - December 5, 2025”Async Enumerable Responses
Section titled “Async Enumerable Responses”// if your endpoint does NOT support server sent events, you can still use IAsyncEnumerable<T> - just remove the IServerSentEventsStream interface[Get("/api/sse")]public class ServerSentEventsRequest : IStreamRequest<EventItem>, IServerSentEventsStream{}v5.1 - October 27, 2025
Section titled “v5.1 - October 27, 2025”[MediatorScopedAttribute]WaitForEvent and EventStream that listens on event publishermediator.RequestServerSentEvents(MyStreamRequest, IHttpContextAccessor) and mediator.EventStreamToServerSentEvents<TEventType>(IHttpContextAccessor)v5.0 - October 25, 2025
Section titled “v5.0 - October 25, 2025”services.AddShinyMediator(x => x.AddRegistry()). We will not call this method automatically, to ensure users are aware of how/where the magic is happeningNew JSON Converter Source Generator
Section titled “New JSON Converter Source Generator”This feature allows you to generate JSON converters at compile time instead of using reflection at runtime. This is especially useful for AOT scenarios like iOS and Blazor WebAssembly. Unfortunately, we are unable to “piggyback” source generators (ours to System.Text.Json), so we found the easiest way to do this was to generate JSON converters, NOT the types emitted by the System.Text.Json source generator.
Setting JSON converters allow you to push JsonConverterAttribute on the type instead of having to pass around a centralized JSON context.
We have two flavours of JSON generation. The first one (and most importantly) is for our HTTP client code generation. NOTE that JsonConverter generation is NOT enabled by default yet.
<ItemGroup> <MediatorHttp Include="OpenApiRemote" Uri="https://youruri" GenerateJsonConverters="true" Visible="false" /></ItemGroup>Secondly, there will be times for features such as offline data storage or caching where data needs to serialize down to disk. These types obviously aren’t generated by our source generators and are likely types you’ve created. We’ve created a secondary way to create JSON converters for your own types.
[SourceGenerateJsonConverter]public partial class YourClass{ public string? Name { get; set; } public int Age { get; set; }}NOTE: Your class must be partial and the attribute must be placed on the type directly.
New Contract Key Source Generator
Section titled “New Contract Key Source Generator”This feature allows you to generate a contract key at compile time instead of using reflection at runtime. This is especially useful for AOT scenarios like iOS and Blazor WebAssembly.
- Type must be partial
- Keys support property names with formatting (like DateTime)
- If no key is specified, it will generate a key format for all public instance properties that are not null
[Shiny.Mediator.ContractKeyAttribute("MyClass_{MyProperty}_{Date:yyyyMMdd}")]public partial class MyClass{ public string? MyProperty { get; set;} public string? UnusedProperty { get; set;} public DateTime? Date { get; set;}}
// UnusedProperty will not be included in the contract key// If a property is null, it is replaced with an empty string// Date will be formatted as yyyyMMdd if not nullJSON Converter Generation
Section titled “JSON Converter Generation”<ItemGroup> <MediatorHttp Include="OpenApiRemote" Uri="https://youruri" GenerateJsonConverters="true" Visible="false" /></ItemGroup>4.9.0 - July 26, 2025
Section titled “4.9.0 - July 26, 2025”<InternetConnectivity /> to your main layout (don’t add more than 1)IContractKeyProvider provides a new injectable and external way of providing contract keys allowing you to pull values from configuration, source generation, etc without using reflection. Our current provider works on reflection to maintain backwards compat, but we have a lot of things planned hereIConnectivityEventHandler that you can implement to receive connectivity events4.8.0 - July 18, 2025
Section titled “4.8.0 - July 18, 2025”InternetConnectivity component that you can add to your pages. It will broadcast to the IConnectivityEventHandler while alive. If you set NoInternetMessage, it will also render the text to your page. Text has no styling. Wrap this component however you want4.7.0 - June 25, 2025
Section titled “4.7.0 - June 25, 2025”<ItemGroup> <MediatorHttp Include="Json File or Name" Uri="If using name above" Namespace="YourNamespace" ContractPostfix="Request" UseInternalClasses="true" GeneratorModelsOnly="true" Visible="false"/></ItemGroup>4.6.3 - 4.6.6 - June 20, 2025
Section titled “4.6.3 - 4.6.6 - June 20, 2025”4.6.2 - June 18, 2025
Section titled “4.6.2 - June 18, 2025”4.6.1 - June 17, 2025
Section titled “4.6.1 - June 17, 2025”4.6.0 - June 16, 2025
Section titled “4.6.0 - June 16, 2025”4.5.0 - June 10, 2025
Section titled “4.5.0 - June 10, 2025”4.4.0 - June 3, 2025
Section titled “4.4.0 - June 3, 2025”Sentry.DiagnosticSource to hook into standard diagnostics4.2.2 - May 23, 2025
Section titled “4.2.2 - May 23, 2025”4.2.1 - May 8, 2025
Section titled “4.2.1 - May 8, 2025”4.2.0 - April 25, 2025
Section titled “4.2.0 - April 25, 2025”4.1.0 - March 28, 2025
Section titled “4.1.0 - March 28, 2025”4.0.4 - March 24, 2025
Section titled “4.0.4 - March 24, 2025”4.0.3 - March 23, 2025
Section titled “4.0.3 - March 23, 2025”4.0.2 - March 14, 2025
Section titled “4.0.2 - March 14, 2025”4.0.1 - March 5, 2025
Section titled “4.0.1 - March 5, 2025”4.0.0 - March 4, 2025
Section titled “4.0.0 - March 4, 2025”Ability to skip source generation using
<PropertyGroup> <ShinyMediatorSkipGenerate>true</ShinyMediatorSkipGenerate></PropertyGroup>3.3.1 - February 20, 2025
Section titled “3.3.1 - February 20, 2025”Headers.BypassExceptionHandling - all Shiny Mediator middleware that error trap will respect this header3.2.0 - January 30, 2025
Section titled “3.2.0 - January 30, 2025”IExceptionHandlerIEventHandler<ConnectivityChanged> to have your services listen to one central location of connectivity change events3.1.2 - January 28, 2025
Section titled “3.1.2 - January 28, 2025”App Support
Section titled “App Support”3.1.0 - January 25, 2025
Section titled “3.1.0 - January 25, 2025”Dapper Extension
Section titled “Dapper Extension”3.0.0 - January 24, 2025
Section titled “3.0.0 - January 24, 2025”2.1.1 - Oct 30, 2024
Section titled “2.1.1 - Oct 30, 2024”2.1.0 - Oct 19, 2024
Section titled “2.1.0 - Oct 19, 2024”2.0.3 - Oct 7, 2024
Section titled “2.0.3 - Oct 7, 2024”2.0.2 - Oct 6, 2024
Section titled “2.0.2 - Oct 6, 2024”2.0.1 - Oct 6, 2024
Section titled “2.0.1 - Oct 6, 2024”2.0.0 - Oct 4, 2024
Section titled “2.0.0 - Oct 4, 2024”IEvent will now publish it automatically1.8.1 - September 14, 2024
Section titled “1.8.1 - September 14, 2024”1.8.0 - September 11, 2024
Section titled “1.8.0 - September 11, 2024”1.7.4 - August 9, 2024
Section titled “1.7.4 - August 9, 2024”1.7.3 - August 7, 2024
Section titled “1.7.3 - August 7, 2024”1.7.2 - July 28, 2024
Section titled “1.7.2 - July 28, 2024”1.7.1 - July 28, 2024
Section titled “1.7.1 - July 28, 2024”1.7.0 - July 20, 2024
Section titled “1.7.0 - July 20, 2024”1.6.0 - July 7, 2024
Section titled “1.6.0 - July 7, 2024”1.5.0 - July 5, 2024
Section titled “1.5.0 - July 5, 2024”1.4.5 - July 3, 2024
Section titled “1.4.5 - July 3, 2024”1.4.0 - June 30, 2024
Section titled “1.4.0 - June 30, 2024”1.3.0 - June 29, 2024
Section titled “1.3.0 - June 29, 2024”[UserExceptionRequestMiddleware] on each request handler methodShiny.Mediator.Middleware.FlushAllStoresEvent in Shiny.Mediator that calls all stores to be flushed1.2.0 - June 19, 2024
Section titled “1.2.0 - June 19, 2024”1.1.0 - June 17, 2024
Section titled “1.1.0 - June 17, 2024”IRequestHandler<T> would not always resolve1.0.0 - June 15, 2024
Section titled “1.0.0 - June 15, 2024”Our initial release - checkout the following: