Dloizides.Subscriptions.AspNetCore 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Dloizides.Subscriptions.AspNetCore --version 1.1.0
                    
NuGet\Install-Package Dloizides.Subscriptions.AspNetCore -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dloizides.Subscriptions.AspNetCore" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dloizides.Subscriptions.AspNetCore" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Dloizides.Subscriptions.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dloizides.Subscriptions.AspNetCore --version 1.1.0
                    
#r "nuget: Dloizides.Subscriptions.AspNetCore, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dloizides.Subscriptions.AspNetCore@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dloizides.Subscriptions.AspNetCore&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Dloizides.Subscriptions.AspNetCore&version=1.1.0
                    
Install as a Cake Tool

Subscriptions.AspNetCore

Reusable platform-subscription billing for multi-tenant ASP.NET Core SaaS.

Pairs Stripe Subscriptions + a plan/feature catalog + a plan gate + a webhook handler. The consuming product brings the persistence layer (ISubscriptionStore) and the routes; everything else is wired in ~10 lines of Program.cs.

Upgrading from ≤ 1.0.5? Read CHANGELOG.md first. The gate in those versions let lapsed paid tenants through and never ran at all under FastEndpoints. 1.1.0 fixes both, so expect callers who previously got a 200 to start correctly getting a 402.

Concepts

  • Plan catalog loaded from configuration. Each plan has a code ("free", "pro", "business"), a rank, a Stripe Price id (free is null), and a feature list.
  • TenantSubscriptionSnapshot is the immutable per-tenant record the feature gates consult.
  • ISubscriptionStore is the per-product persistence layer. Each product implements it against its own DbContext.
  • ISubscriptionService is the high-level API the product calls from endpoints: GetForTenant, CreateCheckoutSession, CreateCustomerPortalSession.
  • The plan gate (PlanPolicy + PlanGate) returns 402 Payment Required when the caller does not qualify. The 402 body carries { error, currentPlan, currentStatus, requiredPlan, upgradePath } so the SPA can route the user to /pricing — and can tell "never subscribed" apart from "card failed". The rule, in one place (PlanGate.MeetsRequiredPlan): plan rank must meet the requirement AND a tenant on a paid plan must be Active/Trialing. A lapsed paid plan is denied; the free tier has nothing to lapse.
  • Stripe webhook handler verifies the HMAC + upserts the snapshot via the store on checkout.session.completed + customer.subscription.* events.

Wiring (consumer side)

Program.cs:

builder.Services.AddPlatformSubscriptions(builder.Configuration);
builder.Services.AddScoped<ISubscriptionStore, MyEfSubscriptionStore>();

// after app.UseAuthorization():
app.MapPlatformSubscriptionsWebhook(); // POST /api/webhooks/stripe-subscriptions

appsettings.json:

{
  "PlatformSubscriptions": {
    "Stripe": {
      "SecretKey": "sk_test_...",
      "WebhookSecret": "whsec_..."
    },
    "Plans": {
      "free":     { "DisplayName": "Free",     "MonthlyPriceEur": 0,  "Rank": 0, "Features": ["1-event"] },
      "pro":      { "DisplayName": "Pro",      "MonthlyPriceEur": 19, "Rank": 10, "StripePriceId": "price_...", "Features": ["unlimited-events", "no-branding", "editor"] },
      "business": { "DisplayName": "Business", "MonthlyPriceEur": 49, "Rank": 20, "StripePriceId": "price_...", "Features": ["unlimited-events", "no-branding", "editor", "team", "priority-support"] }
    }
  }
}

Gating an endpoint

AddPlatformSubscriptions arms the gate on the standard ASP.NET Core authorization pipeline, so it runs under FastEndpoints, minimal APIs and MVC alike. Reference the policy by name:

FastEndpoints (what every dloizides service uses):

public sealed class CreateBatch : Endpoint<BatchRequest, BatchSummary>
{
  public override void Configure()
  {
    Post("/v1/batches");
    Policies(PlanPolicy.For(IchnosPlanCodes.Growth));  // 402 below Growth, or if lapsed
  }
}

Minimal API: app.MapPost(...).RequireAuthorization(PlanPolicy.Pro);

MVC: [Authorize(Policy = PlanPolicy.Pro)]

Policies are materialised on demand from the "plan:<code>" name, so you never register one policy per tier. Anonymous callers get 401, not 402 — "not logged in" is never reported as "you have not paid".

⚠️ The legacy [RequiresPlan] attribute is MVC-only. It is an MVC authorization filter, and FastEndpoints does not invoke MVC filters — an endpoint carrying only that attribute is wide open. It is kept (and fixed) for MVC hosts; use Policies(...) everywhere else. A test in this repo boots a real FastEndpoints host and pins this behaviour so nobody re-learns it the hard way.

What the consumer owns

  • ISubscriptionStore — read + upsert via EF Core (or any persistence). Also FindTenantByStripeCustomerAsync so the webhook can route by customer id.
  • The Stripe Products / Prices in their Stripe dashboard. The package reads the Price ids from config.
  • The Checkout success/cancel URL design (typically per-product).
  • Caching the snapshot if your feature gates fire on every request (the default store is consulted live).

Free / paid tiers

The free tier has no Stripe Subscription — TenantSubscriptionSnapshot.Status stays None. Stripe is involved only when the tenant clicks Upgrade.

Security

  • Webhook signature is verified with Stripe.EventUtility.ConstructEvent.
  • The package does NOT log the Stripe secret key.
  • 402 responses leak only { currentPlan, requiredPlan, upgradePath } — no Stripe ids, no internal state.

Stripe dashboard one-time setup

  1. Create Products: "Pro", "Business".
  2. Add a recurring Price to each (€19/mo, €49/mo). Copy the Price ids.
  3. Configure a Webhook endpoint: https://<your-api>/api/webhooks/stripe-subscriptions. Subscribe to: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted.
  4. Copy the Webhook signing secret.
  5. Drop the four values into the K8s Secret backing PlatformSubscriptions:Stripe:SecretKey, PlatformSubscriptions:Stripe:WebhookSecret, PlatformSubscriptions:Plans:Pro:StripePriceId, PlatformSubscriptions:Plans:Business:StripePriceId.
Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.2.0 97 7/22/2026
1.1.1 185 7/13/2026
1.1.0 116 7/11/2026
1.0.5 176 5/24/2026
1.0.4 102 5/23/2026
1.0.3 108 5/23/2026
1.0.2 98 5/23/2026