Skip to main content

shiny-extensions-push

Generate code using Shiny.Extensions.Push, a server-side push notification dispatch library for .NET with a provider-agnostic core and transports for APNs (.p8/ES256), FCM (HTTP v1), Web Push (VAPID + RFC 8291) and WNS (Windows App SDK / Entra auth), plus Shiny.DocumentDb persistence, structured targeting, topics, interceptors, dead-token pruning, multi-app keyed registration, runtime static/dynamic (multi-tenant) configuration via IPushConfigurationProvider, and System.Diagnostics.Metrics + tracing — fully AOT/trim-safe.

跳到安装

来源信息

仓库
shinyorg/skills
最近来源活动
2026年9月2日 23:39
检测到的 SKILL.md 语言
英语
星标
4
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
shiny-extensions-push
description
Generate code using Shiny.Extensions.Push, a server-side push notification dispatch library for .NET with a provider-agnostic core and transports for APNs (.p8/ES256), FCM (HTTP v1), Web Push (VAPID + RFC 8291) and WNS (Windows App SDK / Entra auth), plus Shiny.DocumentDb persistence, structured targeting, topics, interceptors, dead-token pruning, multi-app keyed registration, runtime static/dynamic (multi-tenant) configuration via IPushConfigurationProvider, and System.Diagnostics.Metrics + tracing — fully AOT/trim-safe.
auto_invoke
true
triggers
["AddPushNotifications","IPushManager","IPushProvider","IPushBatchProvider","IPushRepository","IPushInterceptor","IPushEventReceiver","AddEventReceiver","PushNotification","DeviceRegistration","PushFilter","PushSendResult","PushDeliveryResult","AddApns","ApnsProvider","ApnsOptions","AddFcm","FcmProvider","FcmOptions","AddWebPush","WebPushProvider","WebPushOptions","AddWns","WnsProvider","WnsOptions","WindowsPushOptions","WnsNotificationType","IPushConfigurationProvider","PushConfiguration","UsePushConfiguration","multi-tenant push","multi-tenant configuration","[Truncated]"]
# Shiny.Extensions.Push Skill You are an expert in **Shiny.Extensions.Push**, a server-side push notification dispatch library for .NET. It has a provider-agnostic core, a direct **APNs** transport (token-based `.p8` / ES256 over HTTP/2 — no FCM dependency for Apple), a **Shiny.DocumentDb** persistence option, structured targeting, an interceptor pipeline, automatic dead-token pruning, multi-app keyed registration, and `System.Diagnostics.Metrics` telemetry. The whole library is AOT/trim-safe. ## When to Use This Skill Invoke this skill when the user wants to: - Send push notifications from a .NET **server/backend** (ASP.NET Core, worker, Aspire) to mobile/desktop devices - Register/unregister device tokens and store them (in-memory or Shiny.DocumentDb) - Send to a user across all their devices, to tags/segments, to explicit tokens, or broadcast - Talk to **APNs directly** with a `.p8` auth key (iOS/macOS), **FCM** (HTTP v1) for Android, **Web Push** (VAPID) for browsers, or **WNS** (Windows App SDK / Entra auth) for Windows - Subscribe devices to **topics** and send to a topic - Mutate, localize, personalize, or suppress notifications per-device via interceptors - Observe the send lifecycle (batch start/finish, per-device sent/failed) for telemetry, receipts or dead-letter capture via event receivers - Automatically prune expired/invalid device tokens and apply rotated tokens - Serve **multiple apps** from one server (keyed registrations per provider) - Emit push delivery metrics + traces for OpenTelemetry - Send silent/background (content-available) pushes > This is the **server** counterpart. For the on-device client (registering for push, receiving), use > `Shiny.Push` (the MAUI client library) — a different package. ## Library Overview - **Core namespace**: `Shiny.Extensions.Push` - **NuGet packages**: - `Shiny.Extensions.Push` — core **plus all four transports**: models, `IPushManager`/`IPushProvider`/`IPushRepository`/`IPushInterceptor`, builder/DI, and the APNs, FCM, Web Push and WNS (Windows) providers. The implementation types (`PushManager`, `InMemoryPushRepository`, `DebugPushProvider`, `PushMetrics`) live in the `Shiny.Extensions.Push.Infrastructure` namespace; each transport keeps its own namespace (`Shiny.Extensions.Push.Apns` / `.Fcm` / `.WebPush` / `.Wns`). - `Shiny.Extensions.Push.DocumentDb` — `IPushRepository` backed by Shiny.DocumentDb (any provider) - **Namespaces**: inject the **interfaces** from `Shiny.Extensions.Push` (`IPushManager`, etc.). Add a transport with `using Shiny.Extensions.Push.Apns;` (or `.Fcm` / `.WebPush` / `.Wns`) to surface its `AddApns` extension. You only need `using Shiny.Extensions.Push.Infrastructure;` to name a concrete impl directly (e.g. `AddProvider<DebugPushProvider>()` or casting a repo in tests) — normal app code shouldn't. - **Transports are no longer separate packages.** Do **not** `dotnet add package Shiny.Extensions.Push.Apns` (or `.Fcm` / `.WebPush` / `.Wns`) — they ship inside `Shiny.Extensions.Push`. - **Target**: `net10.0`. AOT/trim-safe. ## Setup (Dependency Injection) ```csharp using Shiny.Extensions.Push; using Shiny.Extensions.Push.Apns; using Shiny.Extensions.Push.DocumentDb; using Shiny.DocumentDb.Sqlite; services.AddPushNotifications(push => { // Direct APNs (single app) push.AddApns(o => { o.TeamId = "ABCDE12345"; o.KeyId = "KEY1234567"; o.BundleId = "com.example.app"; o.PrivateKeyPath = "AuthKey_KEY1234567.p8"; // or o.PrivateKey = "<PEM contents>" // o.ForceEnvironment = PushEnvironment.Sandbox; // default: honour each registration }); // FCM (Android) — Google service-account JSON push.AddFcm(o => o.ServiceAccountJsonPath = "firebase-service-account.json"); // Web Push (browsers) — VAPID keys in base64url (web-push format) push.AddWebPush(o => { o.PublicKey = "<vapid-public-key>"; o.PrivateKey = "<vapid-private-key>"; o.Subject = "mailto:you@example.com"; }); // WNS (Windows) — modern Windows App SDK / Entra (Azure AD) app registration push.AddWns(o => { o.TenantId = "<entra-tenant-id>"; o.ClientId = "<app-registration-client-id>"; o.ClientSecret = "<client-secret>"; }); // Persistence (defaults to in-memory if omitted) push.UseDocumentDb(o => o.DatabaseProvider = new SqliteDatabaseProvider("Data Source=push.db")); // Optional // push.AddInterceptor<LocalizationInterceptor>(); // push.Configure(m => { m.MaxDegreeOfParallelism = 25; m.AutoPruneDeadTokens = true; }); }); ``` Defaults when not configured: an in-memory `IPushRepository` and the built-in `PushManager`. `IMeterFactory`/metrics are wired automatically. ## Registering Devices Resolve `IPushManager` and register the device when your client app reports its token: ```csharp await pushManager.RegisterDevice(new DeviceRegistration { DeviceToken = "<apns-device-token>", Platform = DevicePlatform.iOS, // iOS, MacOS, Android, Windows, WebBrowser DeviceId = "install-guid", // stable identity across token rotation (recommended) UserIdentifier = "user-42", // enables "send to user across all devices" Tags = ["beta", "sports"], Locale = "en-US", Environment = PushEnvironment.Production, // APNs sandbox vs production (tokens are env-specific!) AppId = null // set when using multi-app keyed providers }); await pushManager.UnregisterDevice("<apns-device-token>", DevicePlatform.iOS); ``` ## Sending `IPushManager` returns a `PushSendResult` (`BatchId`, `Sent`, `Failed`, `TokensRemoved`, `Skipped`, `Results`). One device failing never aborts the batch. ```csharp var result = await pushManager.SendToUser("user-42", new PushNotification { Title = "Goal!", Message = "Your team just scored", Badge = 1, Sound = "default", DeepLink = "app://match/123", Priority = PushPriority.High, Data = new Dictionary<string, string> { ["matchId"] = "123" } }); await pushManager.SendToTags(["sports"], notification, TagMatch.Any); await pushManager.SendToTokens(["tokenA", "tokenB"], notification); await pushManager.Broadcast(notification); // Full structured targeting (all set clauses are AND-combined): await pushManager.Send(notification, new PushFilter { Platforms = [DevicePlatform.iOS], Tags = ["beta"], TagMatch = TagMatch.All, Environment = PushEnvironment.Production }); ``` ### Apple-specific options & silent push ```csharp new PushNotification { Title = "Title", Apple = new ApplePushOptions { Subtitle = "Subtitle", Category = "MESSAGE_CATEGORY", ThreadId = "thread-1", MutableContent = true, // for a Notification Service Extension // ContentAvailable = true, // background push // TopicOverride / PushTypeOverride for VoIP/complication topics }, CollapseId = "score-update", // apns-collapse-id TimeToLive = TimeSpan.FromHours(1) // apns-expiration }; // Silent / background push (no visible alert): new PushNotification { Apple = new ApplePushOptions { ContentAvailable = true }, Data = new Dictionary<string, string> { ["sync"] = "inbox" } }; ``` ## Interceptors (localize / personalize / suppress / report) Implement `IPushInterceptor` and register with `push.AddInterceptor<T>()` (additive; run in order). Mutate by replacing `context.Notification`; return `InterceptorResult.Skip` to drop a device. ```csharp public sealed class LocalizationInterceptor : IPushInterceptor { public Task<InterceptorResult> BeforeSend(PushSendContext context, CancellationToken ct = default) { if (context.Registration.Tags.Contains("opted-out")) return Task.FromResult(InterceptorResult.Skip); var title = Localize(context.Notification.Title, context.Registration.Locale); context.Notification = context.Notification with { Title = title }; return Task.FromResult(InterceptorResult.Continue); } public Task OnSent(PushSendContext c, PushDeliveryResult r, CancellationToken ct = default) => Task.CompletedTask; public Task OnFailed(PushSendContext c, PushDeliveryResult r, CancellationToken ct = default) => Task.CompletedTask; } ``` ## Event receivers (observe the send lifecycle — telemetry, receipts, dead-letter) When you just want to *watch* sends — not mutate them — implement `IPushEventReceiver` and register with `push.AddEventReceiver<T>()` (additive; register zero or more). Unlike interceptors, receivers can't skip or mutate; they're pure observers with **batch lifecycle** hooks (`OnBatchStarted`/`OnBatchFinished`) plus per-device `OnSent`/`OnFailed`. A receiver that throws is logged and swallowed — it never breaks a batch. `OnFailed` fires for **every** failed device, including the normalized failures that never throw (`TokenExpired`, `InvalidToken`, `RateLimited`, `Error`) — inspect `result.Status`/`result.Reason`. (Interceptor `Skip` and no-provider outcomes aren't failures; read them from the `OnBatchFinished` `PushSendResult` counts instead.) ```csharp public sealed class PushTelemetry : IPushEventReceiver { public Task OnBatchStarted(Guid batchId, PushFilter filter, PushNotification n, CancellationToken ct = default) => Task.CompletedTask; public Task OnSent(Guid batchId, DeviceRegistration reg, PushNotification n, PushDeliveryResult r, CancellationToken ct = default) => Task.CompletedTask; public Task OnFailed(Guid batchId, DeviceRegistration reg, PushNotification n, PushDeliveryResult r, CancellationToken ct = default) { // capture dead-letters, alert on RateLimited, etc. return Task.CompletedTask; } public Task OnBatchFinished(Guid batchId, PushNotification n, PushSendResult result, CancellationToken ct = default) => Task.CompletedTask; // result.Sent / .Failed / .TokensRemoved / .Skipped } ``` ## Dead-token pruning & rotation (automatic) Providers return a normalized `PushDeliveryStatus`. The manager auto-removes tokens reported `TokenExpired`/`InvalidToken` (APNs `410 Unregistered` / `BadDeviceToken`) and applies rotated tokens (`PushDeliveryResult.UpdatedToken`) to the repository. Disable via `PushManagerOptions.AutoPruneDeadTokens = false`. `RateLimited` is surfaced on the result but **not** retried — backoff is the caller's responsibility. On success, providers set `PushDeliveryResult.ProviderMessageId` (APNs `apns-id`, FCM message `name`, WebPush `Location`). ## Topics Topics are server-side subscriptions (work across every provider, independent of FCM-native topics): ```csharp await pushManager.SubscribeToTopic(token, DevicePlatform.iOS, "sports"); await pushManager.SendToTopic("sports", new PushNotification { Title = "Goal!" }); await pushManager.UnsubscribeFromTopic(token, DevicePlatform.iOS, "sports"); ``` ## Platform routing (FCM / Web Push / WNS) The manager routes a registration to the provider that claims its `Platform`: APNs → `iOS`/`MacOS`, FCM → `Android`, Web Push → `WebBrowser`, WNS → `Windows`. For **WNS**, the registration's `DeviceToken` is the WNS **channel URI**. For **Web Push**, the `DeviceToken` is the subscription endpoint and the `p256dh`/`auth` keys go in `Data`: ```csharp await pushManager.RegisterDevice(new DeviceRegistration { DeviceToken = subscription.Endpoint, Platform = DevicePlatform.WebBrowser, Data = new Dictionary<string, string> { ["p256dh"] = subscription.Keys.P256dh, ["auth"] = subscription.Keys.Auth } }); ``` FCM uses `AndroidPushOptions` on the notification for `android.notification` fields (channel id, icon, color, image); Web Push uses `WebPushOptions` (icon, urgency); WNS uses `WindowsPushOptions` (`Type` = Toast/Tile/Badge/Raw, a verbatim `Payload` for tile/badge, and a toast `Launch` arg — default is a `ToastGeneric` toast built from title/body + `DeepLink`). All providers honour the cross-cutting fields (title/body, badge, sound, data, deep link, collapse id, TTL, priority). ## Delivery fan-out and custom batching FCM's multipart `/batch` endpoint has been discontinued. `FcmProvider` and `FcmTenantProvider` therefore use one supported HTTP v1 `messages:send` request per device; `PushManagerOptions.MaxDegreeOfParallelism` bounds the concurrent fan-out. Per-device dead-token pruning, token rotation, metrics, and `OnSent`/`OnFailed` are preserved. Only an explicit FCM `UNREGISTERED` response expires a token; a generic HTTP 404 is an `Error`. Custom transports with a real bulk endpoint can implement `IPushBatchProvider` (`MaxBatchSize` + `SendBatch` returning one result per registration, in input order). Set `push.Configure(m => m.EnableBatching = false)` to force per-device delivery for those providers. ## Multiple apps (keyed registration) Register one keyed provider per app (works for `AddApns`/`AddFcm`/`AddWebPush`/`AddWns`); devices carry the matching `AppId`; the manager routes by it. ```csharp services.AddPushNotifications(push => { push.AddApns("consumer", o => { o.BundleId = "com.example.consumer"; /* … */ }); push.AddApns("driver", o => { o.BundleId = "com.example.driver"; /* … */ }); }); await pushManager.RegisterDevice(new DeviceRegistration { DeviceToken = "<token>", Platform = DevicePlatform.iOS, AppId = "driver" }); await pushManager.Send(notification, new PushFilter { AppId = "driver", Tags = ["on-shift"] }); ``` ## Runtime configuration provider (dynamic / multi-tenant) The **static** path is the plain `AddApns(o => …)` / `AddApns("key", o => …)` registration (config baked in). For a **dynamic** setup — many apps, or tenants/keys that change without a restart — supply credentials at **send time** via `IPushConfigurationProvider.GetConfiguration(appId)` → `PushConfiguration?` (one record bundling optional `Apns`/`Fcm`/`WebPush`/`Wns` option objects), keyed by `DeviceRegistration.AppId`. Register the provider once with `UsePushConfiguration<T>()`, then opt each transport in with its **no-argument**
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看