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 Features block gets every feature. Exclude on its own keeps that default and simply removes a few.
  • Names cover their children. Sales also matches Sales.Returns. Sales, Sales.*, and Sales* 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" ] with Exclude: [ "Sales.Returns" ] keeps all of Sales on except Sales.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 Financials on a shared interface stays silent for tenants without Financials, 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 Include and Exclude → 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.