Subscrio.Core
1.0.3
dotnet add package Subscrio.Core --version 1.0.3
NuGet\Install-Package Subscrio.Core -Version 1.0.3
<PackageReference Include="Subscrio.Core" Version="1.0.3" />
<PackageVersion Include="Subscrio.Core" Version="1.0.3" />
<PackageReference Include="Subscrio.Core" />
paket add Subscrio.Core --version 1.0.3
#r "nuget: Subscrio.Core, 1.0.3"
#:package Subscrio.Core@1.0.3
#addin nuget:?package=Subscrio.Core&version=1.0.3
#tool nuget:?package=Subscrio.Core&version=1.0.3
Subscrio .NET Core Library
A .NET implementation of the Subscrio subscription management library for managing products, plans, features, customers, subscriptions, and billing cycles.
The entitlement engine that translates subscriptions into feature access.
See the main README for an overview of Subscrio's concepts, architecture, and features.
Installation
Add the NuGet package to your project:
<PackageReference Include="Subscrio.Core" Version="1.0.0" />
Or using the .NET CLI:
dotnet add package Subscrio.Core
Prerequisites:
- .NET 8.0 (LTS - supported until November 2026), .NET 9.0, or .NET 10.0
- PostgreSQL or SQL Server database
The library is multi-targeted: one NuGet package includes builds for all supported frameworks; the correct one is chosen by your project's target framework.
Database and connection
Creating the database
You need a database before using Subscrio. The library does not create the database itself; it only installs and updates schema inside an existing database.
PostgreSQL
- Create a database (e.g.
subscrio) usingpsql, pgAdmin, or your host’s tooling. - Example with
psql:psql -U postgres -c "CREATE DATABASE subscrio;" - Ensure the user in your connection string has rights to create tables and run migrations in that database.
SQL Server
- Create a database (e.g.
Subscrio) in SQL Server Management Studio or with T-SQL:CREATE DATABASE Subscrio; - The login in your connection string must have permission to create tables and run migrations in that database.
Setting the connection string
Set the connection string in one of these ways:
Environment variable (recommended for local and production)
SetDATABASE_URL:- PostgreSQL:
Host=localhost;Port=5432;Database=subscrio;Username=postgres;Password=yourpassword - SQL Server:
Server=localhost;Database=Subscrio;User Id=sa;Password=yourpassword;TrustServerCertificate=true
- PostgreSQL:
ConfigLoader.Load()
ConfigLoader.Load()readsDATABASE_URL(and optionallyDATABASE_TYPE,STRIPE_SECRET_KEY) from the process environment. Use this when you configure the app via env vars or launchSettings.Explicit config in code
Build aSubscrioConfigand setDatabase.ConnectionStringandDatabase.DatabaseType(e.g.DatabaseType.PostgreSQLorDatabaseType.SqlServer). Use this for custom config sources (e.g. key vault, appsettings).ASP.NET Core appsettings
You can load connection strings fromappsettings.jsonor other configuration and pass them into the constructor or into the object you use to buildSubscrioConfig.
After the database exists and the connection string is set, use Database setup and migrations below to install or update the schema.
Building and testing
From the repo root or from core.dotnet:
cd core.dotnet
dotnet build Subscrio.Core.sln
dotnet test Subscrio.Core.sln
Tests expect a running PostgreSQL instance and use the connection string from the TEST_DATABASE_URL environment variable (or a default local connection string). See tests/README.md for test setup.
Quick Start
using Subscrio.Core;
using Subscrio.Core.Config;
// Load configuration
var config = ConfigLoader.Load();
// Initialize the library
var subscrio = new Subscrio(config);
// Install database schema (first time only)
await subscrio.InstallSchemaAsync("your-admin-passphrase");
// Create a product
var product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "my-saas",
DisplayName: "My SaaS Product"
));
// Create a feature
var feature = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-users",
DisplayName: "Maximum Users",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
// Associate feature with product (using keys, not IDs)
await subscrio.Products.AssociateFeatureAsync(product.Key, feature.Key);
// Create a plan (using productKey, not productId)
var plan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "pro-plan",
DisplayName: "Pro Plan"
));
// Set feature value on plan (using keys, not IDs)
await subscrio.Plans.SetFeatureValueAsync(plan.Key, feature.Key, "100");
// Create a billing cycle for the plan (required for subscriptions)
var billingCycle = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: plan.Key,
Key: "monthly",
DisplayName: "Monthly",
DurationValue: 1,
DurationUnit: "months"
));
// Create a customer (using key, not externalId)
var customer = await subscrio.Customers.CreateCustomerAsync(new CreateCustomerDto(
Key: "customer-123",
DisplayName: "Acme Corp"
));
// Create a subscription (using keys and billingCycleKey, not IDs)
var subscription = await subscrio.Subscriptions.CreateSubscriptionAsync(new CreateSubscriptionDto(
Key: "sub-001",
CustomerKey: customer.Key,
BillingCycleKey: billingCycle.Key
));
// Check feature access (requires customerKey, productKey, and featureKey)
var maxUsers = await subscrio.FeatureChecker.GetValueForCustomerAsync(
customer.Key,
product.Key,
"max-users"
);
Console.WriteLine($"Customer can have {maxUsers} users"); // "100"
Dependency Injection
Subscrio supports dependency injection and can be registered in your DI container. This is the recommended approach for web applications and provides better lifetime management.
Quick setup (when you want to use DI)
Add the using for the extension:
using Subscrio.Core.DependencyInjection;
(You may also needusing Microsoft.Extensions.DependencyInjection;forServiceLifetimeif not in scope.)Register Subscrio in your host builder (e.g. in
Program.cs):var config = ConfigLoader.Load(); // or build SubscrioConfig from appsettings/options builder.Services.AddSubscrio(config, ServiceLifetime.Scoped);Inject
Subscriowhere you need it: constructor injection in controllers or services, or as a parameter in minimal API handlers (e.g.app.MapGet("/products", async (Subscrio subscrio) => ...)).
Config from appsettings (optional): You can build SubscrioConfig from your own config (e.g. builder.Configuration or IOptions) and pass it to AddSubscrio. The examples below use ConfigLoader.Load() for simplicity.
See the examples below for full ASP.NET Core, console, and controller usage.
Registration
ASP.NET Core Web API
using Subscrio.Core;
using Subscrio.Core.Config;
using Subscrio.Core.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Load configuration
var config = ConfigLoader.Load();
// Register Subscrio with Scoped lifetime (recommended for web apps)
builder.Services.AddSubscrio(config, ServiceLifetime.Scoped);
var app = builder.Build();
// Use in controllers
app.MapGet("/products", async (Subscrio subscrio) =>
{
var products = await subscrio.Products.ListProductsAsync();
return Results.Ok(products);
});
app.Run();
Console Application with DI
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Subscrio.Core;
using Subscrio.Core.Config;
using Subscrio.Core.DependencyInjection;
var config = ConfigLoader.Load();
var services = new ServiceCollection();
services.AddSubscrio(config, ServiceLifetime.Scoped);
var serviceProvider = services.BuildServiceProvider();
// Use scoped service
using var scope = serviceProvider.CreateScope();
var subscrio = scope.ServiceProvider.GetRequiredService<Subscrio>();
var products = await subscrio.Products.ListProductsAsync();
Controller Injection
using Microsoft.AspNetCore.Mvc;
using Subscrio.Core;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly Subscrio _subscrio;
public ProductsController(Subscrio subscrio)
{
_subscrio = subscrio; // Injected by DI
}
[HttpGet]
public async Task<ActionResult<List<ProductDto>>> GetProducts()
{
var products = await _subscrio.Products.ListProductsAsync();
return Ok(products);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> CreateProduct(CreateProductDto dto)
{
var product = await _subscrio.Products.CreateProductAsync(dto);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
Service Lifetime Options
The AddSubscrio() method accepts a ServiceLifetime parameter:
Scoped (Recommended for Web Applications)
services.AddSubscrio(config, ServiceLifetime.Scoped);
- When to use: ASP.NET Core web applications
- Behavior: One
Subscrioinstance per HTTP request - Benefits:
- Each request gets a fresh
Subscrioinstance - Each instance creates its own
DbContext - Change tracker is clean for each request
- No tracking conflicts between requests
- Proper resource cleanup after request completes
- Each request gets a fresh
Transient
services.AddSubscrio(config, ServiceLifetime.Transient);
- When to use: Console applications, background services, or when you need a new instance each time
- Behavior: New
Subscrioinstance every time it's requested - Benefits: Maximum isolation, each operation gets a fresh instance
Singleton
services.AddSubscrio(config, ServiceLifetime.Singleton);
- When to use: Rarely recommended - only if you want a single instance for the entire application lifetime
- Behavior: One
Subscrioinstance shared across the entire application - Warning: Can cause EF Core tracking conflicts if multiple operations use the same instance
- Not recommended for web applications
How Subscrio Creates DbContext
Subscrio creates its own DbContext internally when instantiated. This means:
- With Scoped lifetime: Each scope (HTTP request) gets a new
Subscrio→ newDbContext→ clean change tracker - With Transient lifetime: Each request gets a new
Subscrio→ newDbContext→ clean change tracker - With Singleton lifetime: Single
Subscrio→ singleDbContext→ potential tracking conflicts
Best Practice: Use ServiceLifetime.Scoped for web applications to ensure proper isolation and resource management.
API Reference
Core Services
subscrio.Products- Product managementsubscrio.Features- Feature flag managementsubscrio.Plans- Subscription plan managementsubscrio.BillingCycles- Billing cycle managementsubscrio.Customers- Customer managementsubscrio.Subscriptions- Subscription lifecyclesubscrio.FeatureChecker- Feature access checkingsubscrio.Stripe- Stripe integration
Instance Methods
InstallSchemaAsync(adminPassphrase)- Install database schemaVerifySchemaAsync()- Check if schema is installed (returns version string or null)MigrateAsync()- Run database migrationsDispose()- Clean up resources (implements IDisposable)
Configuration
Configuration Structure
var config = new SubscrioConfig
{
Database = new DatabaseConfig
{
ConnectionString = "Host=localhost;Port=5432;Database=subscrio;Username=postgres;Password=password",
DatabaseType = DatabaseType.PostgreSQL, // or DatabaseType.SqlServer
Ssl = false,
PoolSize = 10
},
Stripe = new StripeConfig
{
SecretKey = "sk_test_..." // Optional
}
};
Loading Configuration
From Environment Variables
var config = ConfigLoader.Load();
This reads from:
DATABASE_URL- Database connection stringDATABASE_TYPE- "PostgreSQL" or "SqlServer"STRIPE_SECRET_KEY- Stripe secret key (optional)
From Custom Source
var config = new SubscrioConfig
{
Database = new DatabaseConfig
{
ConnectionString = "your-connection-string",
DatabaseType = DatabaseType.PostgreSQL
}
};
Database setup and migrations
Summary of how to get the database ready and keep it up to date:
| Step | When | What to do |
|---|---|---|
| 1. Create DB | Once | Create an empty PostgreSQL or SQL Server database (see Database and connection). |
| 2. Connection string | Once | Set DATABASE_URL (or pass SubscrioConfig with Database.ConnectionString). |
| 3. Install schema | First run only | Call InstallSchemaAsync(adminPassphrase) to create all tables and seed the admin passphrase. |
| 4. Migrate | After library updates | Call MigrateAsync() to apply any new migrations. |
First-time schema installation
var subscrio = new Subscrio(config);
await subscrio.InstallSchemaAsync("your-secure-admin-passphrase");
This creates every required table and stores the hashed admin passphrase. Run it once per database.
Checking if schema is installed
var version = await subscrio.VerifySchemaAsync();
if (version == null)
{
await subscrio.InstallSchemaAsync("your-admin-passphrase");
}
Use this on startup if you want to install the schema only when it is missing.
Running migrations
After upgrading the Subscrio library, run migrations so the database schema stays in sync:
var migrationsApplied = await subscrio.MigrateAsync();
Console.WriteLine($"Applied {migrationsApplied} migrations");
Call MigrateAsync() during startup or as part of your deployment; it is safe to call when there are no pending migrations (it will apply zero and return 0).
Usage Examples
Create a Product with Features
// Create product
var product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "saas-platform",
DisplayName: "SaaS Platform"
));
// Create features
var maxProjects = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-projects",
DisplayName: "Max Projects",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
var ganttCharts = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "gantt-charts",
DisplayName: "Gantt Charts",
ValueType: FeatureValueType.Toggle,
DefaultValue: "false"
));
// Associate features with product
await subscrio.Products.AssociateFeatureAsync(product.Key, maxProjects.Key);
await subscrio.Products.AssociateFeatureAsync(product.Key, ganttCharts.Key);
Create Plans with Feature Values
// Create plan
var basicPlan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "basic",
DisplayName: "Basic Plan"
));
var proPlan = await subscrio.Plans.CreatePlanAsync(new CreatePlanDto(
ProductKey: product.Key,
Key: "pro",
DisplayName: "Pro Plan"
));
// Set feature values for plans
await subscrio.Plans.SetFeatureValueAsync(proPlan.Key, maxProjects.Key, "50");
await subscrio.Plans.SetFeatureValueAsync(proPlan.Key, ganttCharts.Key, "true");
Create Billing Cycles
// Create monthly billing cycle
var monthly = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: proPlan.Key,
Key: "pro-monthly",
DisplayName: "Pro Monthly",
DurationValue: 1,
DurationUnit: "months"
));
// Create yearly billing cycle
var yearly = await subscrio.BillingCycles.CreateBillingCycleAsync(new CreateBillingCycleDto(
PlanKey: proPlan.Key,
Key: "pro-yearly",
DisplayName: "Pro Yearly",
DurationValue: 1,
DurationUnit: "years"
));
Create Customers and Subscriptions
// Create customer
var customer = await subscrio.Customers.CreateCustomerAsync(new CreateCustomerDto(
ExternalId: "customer-123",
DisplayName: "John Doe"
));
// Create subscription
var subscription = await subscrio.Subscriptions.CreateSubscriptionAsync(new CreateSubscriptionDto(
Key: "sub-001",
CustomerKey: customer.Key,
BillingCycleKey: monthly.Key
));
// Add feature override
await subscrio.Subscriptions.AddFeatureOverrideAsync(
subscription.Key,
maxProjects.Key,
"100",
OverrideType.Permanent
);
Check Feature Values
// Check if feature is enabled
var hasGanttCharts = await subscrio.FeatureChecker.IsEnabledAsync(
customer.ExternalId,
ganttCharts.Key
);
// Get feature value
var maxProjectsValue = await subscrio.FeatureChecker.GetValueAsync(
customer.ExternalId,
maxProjects.Key
);
// Get all features
var allFeatures = await subscrio.FeatureChecker.GetAllFeaturesAsync(customer.ExternalId);
Feature Resolution Hierarchy
Feature values are resolved using a smart hierarchy. See the main README for details.
Stripe Integration
Subscrio integrates with Stripe for payment processing. See the main README for an overview.
Important: Subscrio does NOT verify Stripe webhook signatures. You must verify signatures before passing events to Subscrio.
Process Stripe Webhooks
using Stripe;
// In your webhook endpoint (after verifying Stripe signature)
[HttpPost("webhooks/stripe")]
public async Task<IActionResult> HandleStripeWebhook()
{
var json = await new StreamReader(Request.Body).ReadToEndAsync();
var stripeSignature = Request.Headers["Stripe-Signature"].ToString();
try
{
// Verify webhook signature
var stripeEvent = EventUtility.ConstructEvent(
json,
stripeSignature,
_configuration["Stripe:WebhookSecret"]
);
// Process verified event
await _subscrio.Stripe.ProcessStripeEventAsync(stripeEvent);
return Ok(new { received = true });
}
catch (StripeException ex)
{
return BadRequest(new { error = ex.Message });
}
}
Create Stripe Subscription
var subscription = await subscrio.Stripe.CreateStripeSubscriptionAsync(
customerExternalId: customer.ExternalId,
planKey: proPlan.Key,
renewalCycleKey: monthly.Key
);
.NET Support
Full .NET support with comprehensive type definitions and async/await patterns:
using Subscrio.Core;
using Subscrio.Core.Dtos;
// All APIs are fully typed
ProductDto product = await subscrio.Products.CreateProductAsync(new CreateProductDto(
Key: "my-product",
DisplayName: "My Product"
));
// DTOs are strongly typed
var feature = await subscrio.Features.CreateFeatureAsync(new CreateFeatureDto(
Key: "max-projects",
DisplayName: "Max Projects",
ValueType: FeatureValueType.Numeric,
DefaultValue: "10"
));
Key Concepts
Keys vs IDs: All public APIs use keys (string identifiers like "my-product") rather than internal IDs. Keys are:
- Human-readable and memorable
- Globally unique within their scope
- Immutable once created
- Used in all method calls and references
DTOs: All create/update operations use DTOs (Data Transfer Objects) with validation:
CreateProductDto,CreateFeatureDto,CreatePlanDto, etc.- All fields are validated before processing
- Type-safe with full C# type inference
Async/Await: All operations are asynchronous:
- All methods return
Task<T>orTask - Use
awaitfor all Subscrio operations - Proper async/await patterns throughout
Dependency Injection: Recommended for web applications:
- Register with
AddSubscrio()extension method - Use
ServiceLifetime.Scopedfor web apps - Automatic resource management
Best Practices
- Use Dependency Injection - Register Subscrio with
AddSubscrio()in web applications - Scoped Lifetime - Use
ServiceLifetime.Scopedfor web apps to avoid tracking conflicts - Schema Installation - Run
InstallSchemaAsync()once during application startup - Error Handling - Handle
ValidationException,NotFoundException,ConflictExceptionappropriately - Feature Keys - Use lowercase alphanumeric keys with hyphens (e.g.,
max-projects) - Customer External IDs - Use your own customer identifiers for easy integration
- Database Connections - Subscrio manages its own DbContext lifecycle based on service lifetime
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
Support
| 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
- BCrypt.Net-Next (>= 4.0.3)
- FluentValidation (>= 11.9.0)
- FluentValidation.DependencyInjectionExtensions (>= 11.9.0)
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.SqlServer (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Newtonsoft.Json (>= 13.0.3)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.0)
- Riok.Mapperly (>= 3.5.0)
- Stripe.net (>= 45.0.0)
-
net8.0
- BCrypt.Net-Next (>= 4.0.3)
- FluentValidation (>= 11.9.0)
- FluentValidation.DependencyInjectionExtensions (>= 11.9.0)
- Microsoft.EntityFrameworkCore (>= 8.0.11)
- Microsoft.EntityFrameworkCore.SqlServer (>= 8.0.11)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Newtonsoft.Json (>= 13.0.3)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 8.0.8)
- Riok.Mapperly (>= 3.5.0)
- Stripe.net (>= 45.0.0)
- System.Text.Json (>= 9.0.0)
-
net9.0
- BCrypt.Net-Next (>= 4.0.3)
- FluentValidation (>= 11.9.0)
- FluentValidation.DependencyInjectionExtensions (>= 11.9.0)
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.SqlServer (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Newtonsoft.Json (>= 13.0.3)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 9.0.2)
- Riok.Mapperly (>= 3.5.0)
- Stripe.net (>= 45.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.