Features
SaaS feature management with three value types — toggle (boolean), numeric (bounded), and selection (enum). Features cascade through a Tenant → Plan → Default chain and support gating via attributes, middleware, and imperative checks.
Package structure
Section titled “Package structure”DirectoryGranit.Features/ SaaS feature management (Toggle/Numeric/Selection)
- Granit.Features.EntityFrameworkCore Isolated FeaturesDbContext
| Package | Role | Depends on |
|---|---|---|
Granit.Features | IFeatureChecker, IFeatureLimitGuard, plan-based cascade | Granit.Caching, Granit.Localization |
Granit.Features.EntityFrameworkCore | FeaturesDbContext (isolated) | Granit.Features, Granit.Persistence |
Dependency graph
Section titled “Dependency graph”graph TD
F[Granit.Features] --> CA[Granit.Caching]
F --> L[Granit.Localization]
FEF[Granit.Features.EntityFrameworkCore] --> F
FEF --> P[Granit.Persistence]
[DependsOn(typeof(GranitFeaturesModule))]public class AppModule : GranitModule { }Uses InMemoryFeatureStore by default (no persistence).
[DependsOn( typeof(GranitFeaturesModule), typeof(GranitFeaturesEntityFrameworkCoreModule))]public class AppModule : GranitModule { }builder.AddGranitFeaturesEntityFrameworkCore(opt => opt.UseNpgsql(connectionString));Defining features
Section titled “Defining features”Implement IFeatureDefinitionProvider to declare features. Features are grouped for
admin UI organization.
public sealed class AcmeFeatureDefinitionProvider : FeatureDefinitionProvider{ public override void Define(IFeatureDefinitionContext context) { FeatureGroupDefinition group = context.AddGroup("Acme", "Acme Application");
group.AddToggle("Acme.VideoConsultation", defaultValue: "false", displayName: "Video Consultation");
group.AddNumeric("Acme.MaxUsersCount", defaultValue: "50", displayName: "Maximum Users", numericConstraint: new NumericConstraint(Min: 1, Max: 10_000));
group.AddSelection("Acme.StorageTier", defaultValue: "standard", displayName: "Storage Tier", selectionValues: new SelectionValues("standard", "premium", "enterprise")); }}FeatureDefinition properties
Section titled “FeatureDefinition properties”| Property | Description |
|---|---|
Name | Unique feature name (convention: Module.FeatureName) |
DefaultValue | Fallback string value |
ValueType | Toggle, Numeric, or Selection |
NumericConstraint | Min/Max bounds for Numeric features |
SelectionValues | Allowed values for Selection features |
Value cascade
Section titled “Value cascade”Features are resolved through a Tenant → Plan → Default cascade:
Tenant override → Plan value → Default (code)The application must implement IPlanIdProvider and IPlanFeatureStore to activate
plan-level resolution. Without these, features fall back to their default values.
IFeatureChecker
Section titled “IFeatureChecker”The main abstraction for querying feature state at runtime:
public class ConsultationService(IFeatureChecker features){ public async Task StartAsync(CancellationToken ct) { // Gate: throws FeatureNotEnabledException (HTTP 403) if disabled await features .RequireEnabledAsync("Acme.VideoConsultation", ct) .ConfigureAwait(false);
// Conditional logic bool isEnabled = await features .IsEnabledAsync("Acme.VideoConsultation", ct) .ConfigureAwait(false);
// Numeric value long maxUsers = await features .GetNumericAsync("Acme.MaxUsersCount", ct) .ConfigureAwait(false); }}IFeatureLimitGuard
Section titled “IFeatureLimitGuard”Enforces numeric feature limits before mutating operations. Throws
FeatureLimitExceededException (HTTP 403) when the limit is reached.
public sealed class CreatePatientHandler( IFeatureLimitGuard limitGuard, IPatientRepository patients){ public async Task HandleAsync(CreatePatientCommand cmd, CancellationToken ct) { long current = await patients.CountAsync(ct).ConfigureAwait(false); await limitGuard .CheckAsync("Acme.MaxUsersCount", current, ct) .ConfigureAwait(false);
// ... proceed with creation }}Feature gating
Section titled “Feature gating”app.MapPost("/consultations", handler) .RequiresFeature("Acme.VideoConsultation");[RequiresFeature("Acme.VideoConsultation")]public IActionResult StartConsultation() => Ok();// Decorate the message class[RequiresFeature("Acme.ExportPdf")]public sealed class GenerateExportCommand { }Register RequiresFeatureMiddleware in your Wolverine setup.
When the feature is disabled, FeatureNotEnabledException is thrown and mapped to
HTTP 403 with errorCode: "Features:NotEnabled".
Feature store abstractions
Section titled “Feature store abstractions”public interface IFeatureStoreReader{ Task<string?> GetOrNullAsync( string featureName, string? tenantId, CancellationToken cancellationToken = default);}
public interface IFeatureStoreWriter{ Task SetAsync( string featureName, string? tenantId, string value, CancellationToken cancellationToken = default);
Task DeleteAsync( string featureName, string? tenantId, CancellationToken cancellationToken = default);}Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Modules | GranitFeaturesModule, GranitFeaturesEntityFrameworkCoreModule | — |
| Abstractions | IFeatureChecker, IFeatureLimitGuard, IFeatureStoreReader, IFeatureStoreWriter | Granit.Features |
| Definitions | FeatureDefinition, FeatureDefinitionProvider, FeatureValueType, NumericConstraint, SelectionValues | Granit.Features |
| Gating | [RequiresFeature], .RequiresFeature(), RequiresFeatureMiddleware | Granit.Features |
See also
Section titled “See also”- Settings — cascading key-value settings
- Multi-tenancy — tenant context for feature resolution
- Caching — used for feature value caching