Mostlylucid.SignalShingle
1.0.0
dotnet add package Mostlylucid.SignalShingle --version 1.0.0
NuGet\Install-Package Mostlylucid.SignalShingle -Version 1.0.0
<PackageReference Include="Mostlylucid.SignalShingle" Version="1.0.0" />
<PackageVersion Include="Mostlylucid.SignalShingle" Version="1.0.0" />
<PackageReference Include="Mostlylucid.SignalShingle" />
paket add Mostlylucid.SignalShingle --version 1.0.0
#r "nuget: Mostlylucid.SignalShingle, 1.0.0"
#:package Mostlylucid.SignalShingle@1.0.0
#addin nuget:?package=Mostlylucid.SignalShingle&version=1.0.0
#tool nuget:?package=Mostlylucid.SignalShingle&version=1.0.0
Mostlylucid.SignalShingle
Mostlylucid.SignalShingle is a bounded, demand-driven projection cache for shared live surfaces. Requests only read the last successful value (or receive Warming); a scheduled materializer is the only component that performs expensive work.
It fits operational dashboards, shared widgets, status pages, and read-heavy server-rendered applications where coherent, honest snapshots matter more than computing on every request.
Read the full design rationale: Signal Shingle architecture.
Why it exists
Traditional GetOrCreate caching lets a cold request execute expensive work. Under concurrent traffic that becomes a fan-out storm. Signal Shingle makes the boundary explicit:
- The request records short-lived demand and reads a snapshot.
- A bounded scheduler acquires due work, batches/composes it, then completes a lease.
- SignalR sends a tiny dirty beacon; the browser fetches only the updated HTML fragment.
The cache is not a data authority. It contains derived projections only.
services.AddSignalShingleCache<DashboardEnvelope, DashboardModel>(o =>
{
o.Capacity = 256;
o.MaximumStaleness = TimeSpan.FromMinutes(20);
});
// Request path: no factory and therefore no accidental database work.
var read = cache.Read(envelope,
SignalShingleDemand.Create("dashboard:traffic", TimeSpan.FromMinutes(1)));
return read.IsWarm ? Render(read.Value!) : RenderWarming();
// Scheduled, bounded materializer: batch/compose these candidates as appropriate.
foreach (var candidate in cache.AcquireRefreshCandidates(maxCount: 16))
{
try { cache.CompleteRefresh(candidate, await ComposeAsync(candidate.Key, ct), generation); }
catch { cache.FailRefresh(candidate); throw; }
}
The caller owns key normalization and the refresh executor. The core package owns bounded LFU-style retention, demand leases, pinned coverage, stale fallback, generation ordering, and exclusive refresh leases.
ASP.NET Core quick start
builder.Services.AddSignalShingleUi(o => o.Capacity = 128);
app.MapSignalShingleUi();
Add the package script plus Alpine and the SignalR browser client, then use a cache island in any Razor view:
@addTagHelper *, Mostlylucid.SignalShingle
<signal-shingle key="traffic" consumer="dashboard:traffic" refresh-seconds="30">
<p>Warming dashboard…</p>
</signal-shingle>
<script src="/_content/Mostlylucid.SignalShingle/signal-shingle.js"></script>
The scheduled materializer calls CompleteRefresh; ISignalShingleNotifier.NotifyAsync emits the small SignalR dirty beacon after a successful completion. External source changes can call MarkDirtyAndNotifyAsync.
The demo project is runnable with dotnet run --project src/Mostlylucid.SignalShingle.Demo.
Guarantees
- No request-time compute. There is no factory API on
Read. - One refresh per key. Acquiring candidates creates a short refresh lease; overlapping scheduler waves cannot both compose a key.
- No lost invalidations. A dirty event received during a refresh increments a dirty version, so the older completion cannot acknowledge it.
- Honest age. Values older than
MaximumStalenessreturnWarming, even if they remain resident for diagnostics. - Bounded local state. Pins protect known defaults; active demand is LFU-ranked and expires through renewable leases.
Integration notes
- Normalize every result-bearing parameter into the key. Do not include refresh cadence in the key.
- Use a stable consumer name per widget/view, not a random request id, so requests renew one demand lease.
- Always call
FailRefreshwhen composition fails; this releases the work for a later wave. - Treat SignalR as an acceleration hint. Reconnects and normal cadence must still recover missed beacons.
- The HTML cache is local to a replica. Do not store secrets in fragment HTML or log its contents.
See architecture notes, ASP.NET Core integration, and NuGet publishing.
Contract
- A read never invokes a factory or starts background work.
- Refresh work is exclusively leased; completion only acknowledges the invalidations it observed.
- Cadence is the minimum interval across active leases and an optional pin; it is not key material.
- Values past
MaximumStalenessbecomeWarming, rather than being presented as current. - Pins cover known defaults; demand leases retain views that real consumers continue to read.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 133 | 7/25/2026 |