Feature management
One server. Many tenants (companies). Each tenant chooses which features are ON. A feature that is OFF disappears for that tenant only — no tables, no rules, hidden from the API — and turning it back ON brings everything back. Nothing is deleted.
Two things are called "Feature" in Core — don't mix them up. This page is about turning features on and off per tenant (configuration). Features is about declaring a feature and its registration order (
IFeature,[FeatureDependsOn]). Both build on the "one folder = one feature" idea.
flowchart LR
S["One server<br/>every feature built in"]
S --> A["Company A<br/>Financials: ON"]
S --> B["Company B<br/>Financials: OFF"]
A --> A1["has Financials tables<br/>runs Financials rules<br/>sees it in the API"]
B --> B1["no Financials tables<br/>skips Financials rules<br/>can't see it at all"]
A feature is a folder
The folder (namespace) a class lives in is its feature. You don't tag classes with a feature attribute.
Financials/
├── Invoice.cs ← feature "Financials"
└── Ledger.cs ← feature "Financials"
Sales/
└── Order.cs ← feature "Sales"
Features nest with their namespaces: Sales.Returns is a child of Sales, so turning off Sales turns off everything under it too.
See Features for declaring a feature class and controlling its order, and Feature in a box for organizing code this way.
How it works
At startup Core discovers every feature. For each tenant, its configuration becomes a simple set of on/off choices. Every request then runs against only that tenant's ON features:
flowchart LR
Req["Request comes in"] --> Who["Which tenant?"]
Who --> Set["its ON/OFF choices"]
Set --> Model["database model<br/>(OFF tables left out)"]
Set --> Meta["API schema<br/>(OFF entities left out)"]
Set --> Run["business rules<br/>(OFF rules skipped)"]
Tenants that made the same choices share the same built model and schema, so that work happens once and is reused — 100 companies with 3 different feature choices means 3 builds, not 100. And a tenant with every feature ON behaves exactly like a server that never heard of features: there is no extra cost for using all of it.
Configure which features exist
The global Features section holds the platform-wide settings.
{
"Features": {
"NamespaceTrimRoots": [ "Benevia.ERP.Model" ],
"AlwaysEnabled": [ "Platform" ]
}
}
| Setting | Purpose |
|---|---|
NamespaceTrimRoots |
Prefixes stripped from namespaces to produce short feature names. With Benevia.ERP.Model trimmed, the namespace Benevia.ERP.Model.Financials becomes the feature Financials. |
AlwaysEnabled |
Features every tenant always has and can never turn off (for example, core platform features). Trying to exclude one of these fails startup. |
Configure features per tenant
Each tenant turns features on or off under Tenants:<id>:Features, using two lists:
{
"Tenants": {
"acme": { "Features": { "Include": [ "Financials", "Sales" ] } },
"hillside": { "Features": { "Exclude": [ "Financials" ] } }
}
}
| List | Meaning |
|---|---|
Exclude |
A block list. Everything stays on except what you list. |
Include |
An allow list. The moment you use Include, everything is off except what you list (plus always-on features and anything they depend on). |
How a feature name is matched:
- Default is ON. A tenant with no
Featuresblock gets every feature.Excludeon its own keeps that default and simply removes a few. - Names cover their children.
Salesalso matchesSales.Returns.Sales,Sales.*, andSales*all mean the same thing. - Most specific wins. A feature is matched from its own name up through its parents, and the first list it appears in decides. So
Include: [ "Sales" ]withExclude: [ "Sales.Returns" ]keeps all ofSaleson exceptSales.Returns. - The same name in both lists fails startup with a clear error — remove it from one list.
Feature names use the trimmed path (
Financials), not the full namespace.
Depend on other features
A feature can require another. Declare it once on the feature class:
[FeatureDependsOn(typeof(Products.Feature), typeof(Customers.Feature))]
public class Feature : IFeature { /* ... */ }
If a tenant turns on a feature but leaves out something it needs, Core turns the dependency back on automatically and logs it. Namespace nesting counts as a dependency too: a child always requires its parent. See Features for declaring dependencies.
What turns off when a feature is OFF
For a tenant with the feature OFF, everything the feature owns is gone — not hidden behind a permission, actually absent:
| Area | Behavior when the feature is OFF |
|---|---|
| Entity endpoint | GET /api/<Entity> returns 404 — it does not exist for this tenant |
| Writing a disabled property | A POST/PATCH that sets it returns 400 |
| Querying a disabled property | $select/$filter/$orderby naming it returns 400 |
| Database | The entity's table and any disabled columns are left out of the tenant's model |
| API schema | $metadata (OData) and entitymetadata (app) omit the entity/property; each distinct feature set gets its own ETag |
| Client / UI | Graphs, property groups, lists, and pickers use the tenant-trimmed metadata, so disabled entities/properties do not render or get requested. Free-form ExtensionRegion UI renders only when its optional IRegionExtension.Feature is present in that metadata; no client appsetting flag is needed. |
| Permissions | Disabled entities and properties drop out of the permissions payload |
| Seed / demo data | Generators for disabled entities are skipped |
| Required fields | A required field owned by an OFF feature is skipped, so a tenant is never blocked by a field it cannot see |
| Business rules | Compute, Validate, Changed, Added, Deleting, PreSave and other subscribers owned by the feature do not run |
Two behaviors worth knowing:
- Shared logic on an interface is gated by the feature that wrote the rule, not by the entity. A rule authored in
Financialson a shared interface stays silent for tenants withoutFinancials, even on entities from other features — while a rule authored elsewhere on that same interface keeps running. - Compute chains fall back. If a disabled compute would have produced a value, the next enabled compute below it produces it instead. You get a real value, not a blank.
Turning a feature off is reversible
Disabling a feature never drops data. The tables and columns it owns are marked deleted in place — left where they are, with their rows — so nothing is lost (and an older server that still has the feature keeps working). Turn the feature back on and they are restored, including any rows added while it was off. A genuine model removal is marked the same way, but it also runs the deletion subscribers ([EntityDeleted]/[PropertyDeleted]) so a consuming app can migrate the data out; a reversible feature toggle never fires them.
Not to be confused with feature version upgrades, a separate mechanism for versioned one-time data changes within a feature.
Safety and validation
Core fails fast on bad configuration, so mistakes surface at startup rather than in production:
- Unknown or misspelled feature → the host will not start, with a "did you mean
Financials?" suggestion. - Excluding an always-on feature → the host will not start.
- The same feature in
IncludeandExclude→ the host will not start. - Bad config on a live reload → the last good configuration is kept and the error is logged.
- Unknown or missing tenant → treated as "everything on" rather than "nothing on", so a lookup failure never silently hides data. Within a request, a mismatched tenant claim is refused rather than served another tenant's model.
FAQ
How do I create a feature? Put its classes in their own folder/namespace. Optionally add a Feature class (see Features) to name it and set its order.
What can a tenant never turn off? Anything listed in Features:AlwaysEnabled, plus core platform tables (identity, schema versioning) that are not part of any toggleable feature.
Does turning a feature off delete data? No. Its tables and columns are marked deleted in place and restored when the feature is re-enabled.
What does a user see for an off feature? Nothing — its endpoints return 404 and it is absent from the schema, as if it were never built.
How is this different from Features? That page declares a feature and its registration order. This page turns features on and off per tenant. Same feature identity (the folder), different job.
Is [FeatureVersionChanged] the same thing? No — that is a versioned one-time data upgrade within a feature. See Feature version upgrades.
Related
- Features — declare features and control registration order
- Feature in a box — organize code as one folder per feature
- Configure your APP — set up tenants and services
- Role-based access control — permissions, applied after features