doc/WebSite/Feature-Management.md
Most SaaS (multi-tenant) applications have editions (packages) that have different features. This way, they can provide different price and feature options to their tenants (customers).
ASP.NET Boilerplate provides a feature system to make it easier. You can define features, check if a feature is enabled for a tenant, and integrate the feature system to other ASP.NET Boilerplate concepts (like authorization and navigation).
The feature system uses the IFeatureValueStore to get the values of features. While you can implement it in your own way, it's fully implemented in the Module Zero project. If it's not implemented, NullFeatureValueStore is used to return null for all features (so the default feature values are used in this case).
There are two fundamental feature types.
Can be "true" or "false". This type of a feature can be enabled or disabled (for an edition or for a tenant).
Can be an arbitrary value. While it's stored and retrieved as a string, numbers also can be stored as strings.
For example, our application may be a task management application and we may have a limit for creating tasks in a month. Imagine that we have two different editions/packages; one allows for creating 1,000 tasks per month, while the other allows for creating 5,000 tasks per month. This feature should be stored as a value, not simply as true or false.
A feature should be defined before it is checked. A module can define its own features by deriving from the FeatureProvider class. Here's a very simple feature provider that defines 3 features:
public class AppFeatureProvider : FeatureProvider
{
public override void SetFeatures(IFeatureDefinitionContext context)
{
var sampleBooleanFeature = context.Create("SampleBooleanFeature", defaultValue: "false");
sampleBooleanFeature.CreateChildFeature("SampleNumericFeature", defaultValue: "10");
context.Create("SampleSelectionFeature", defaultValue: "B");
}
}
After creating a feature provider, we must register it in our module's PreInitialize method as shown below:
Configuration.Features.Providers.Add<AppFeatureProvider>();
A feature definition requires at least two properties:
Here, we defined a boolean feature named "SampleBooleanFeature", with the default value of "false" (not enabled). We also defined two value features. Note that SampleNumericFeature is defined as a child of SampleBooleanFeature.
Tip: Create a const string for a feature name and use it everywhere to prevent typing errors.
While the unique name and default value properties are required, there are some optional properties for more fine-tuned control.
Let's see some more detailed definitions for the features above:
public class AppFeatureProvider : FeatureProvider
{
public override void SetFeatures(IFeatureDefinitionContext context)
{
var sampleBooleanFeature = context.Create(
AppFeatures.SampleBooleanFeature,
defaultValue: "false",
displayName: L("Sample boolean feature"),
inputType: new CheckboxInputType()
);
sampleBooleanFeature.CreateChildFeature(
AppFeatures.SampleNumericFeature,
defaultValue: "10",
displayName: L("Sample numeric feature"),
inputType: new SingleLineStringInputType(new NumericValueValidator(1, 1000000))
);
context.Create(
AppFeatures.SampleSelectionFeature,
defaultValue: "B",
displayName: L("Sample selection feature"),
inputType: new ComboboxInputType(
new StaticLocalizableComboboxItemSource(
new LocalizableComboboxItem("A", L("Selection A")),
new LocalizableComboboxItem("B", L("Selection B")),
new LocalizableComboboxItem("C", L("Selection C"))
)
)
);
}
private static ILocalizableString L(string name)
{
return new LocalizableString(name, AbpZeroTemplateConsts.LocalizationSourceName);
}
}
Note that the Input type definitions are not used by ASP.NET Boilerplate. They can be used by applications to create inputs for features. ASP.NET Boilerplate just provides the infrastructure to make it easier.
As shown in the sample feature providers, a feature can have child features. A Parent feature is generally defined as a boolean feature. Child features will be available only if the parent is enabled. ASP.NET Boilerplate does not enforce this, but we recommend it. The application should take care of it.
We define a feature to check its value in the application to allow or block some application features per tenant. There are different ways of checking it.
We can use the RequiredFeature attribute for a method or a class as shown below:
[RequiresFeature("ExportToExcel")]
public async Task<FileDto> GetReportToExcel(...)
{
...
}
This method is executed only if the "ExportToExcel" feature is enabled for the current tenant (current tenant is obtained from IAbpSession). If it's not enabled, an AbpAuthorizationException is thrown automatically.
As such, the RequiresFeature attribute should only be used for boolean type features. Otherwise, you may get exceptions.
ASP.NET Boilerplate uses the power of dynamic method interception for feature checking. There are some restrictions for the methods that can use the RequiresFeature attribute.
Also,
We can inject and use IFeatureChecker to check a feature manually (it's automatically injected and directly usable for application services, MVC, and Web API controllers).
This is used to simply check if a given feature is enabled or not. Example:
public async Task<FileDto> GetReportToExcel(...)
{
if (await FeatureChecker.IsEnabledAsync("ExportToExcel"))
{
throw new AbpAuthorizationException("You don't have this feature: ExportToExcel");
}
...
}
The IsEnabledAsync and other methods also have sync versions.
The IsEnabled method should be used for boolean type features, otherwise you may get exceptions.
If you just want to check a feature and throw an exception as shown in the example, you can use the CheckEnabled method.
Used to get the current value of a feature for value-type features. Example:
var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth();
if (createdTaskCountInThisMonth >= FeatureChecker.GetValue("MaxTaskCreationLimitPerMonth").To<int>())
{
throw new AbpAuthorizationException("You exceed task creation limit for this month, sorry :(");
}
The FeatureChecker methods also have overrides to check features not only for the current tenantId, but for a specified tenantId as well.
The base view class defines the IsFeatureEnabled method to check if the current feature enabled.
@if (IsFeatureEnabled("App.ChatFeature"))
{
<div class="row text-end mb-3">
<div class="col">
<button class="btn btn-primary" id="AddFriendButton">Add Friend</button>
</div>
</div>
}
@if (CurrentUser.Friends.Count() < Convert.ToInt32(GetFeatureValue("App.FriendLimit")))
{
<div class="row text-end mb-3">
<div class="col">
<button class="btn btn-primary" id="AddFriendButton">("AddFriendButton")</button>
</div>
</div>
}
In the client side, we can use the abp.features namespace to get the current values of features.
var isEnabled = abp.features.isEnabled('SampleBooleanFeature');
var value = abp.features.getValue('SampleNumericFeature');
If you enabled Multi-Tenancy, then you can also ignore feature check for host users by configuring it in PreInitialize method of our module as shown below:
Configuration.MultiTenancy.IgnoreFeatureCheckForHostUsers = true;
Note: IgnoreFeatureCheckForHostUsers default value is false;
If you need the definitions of features, you can inject and use IFeatureManager.
The ASP.NET Boilerplate framework does not have a built-in edition system because such a system requires a database (to store editions, edition features, tenant-edition mappings and so on...). Therefore, the edition system is implemented in Module Zero. You can use it as a ready-made edition system or implement one yourself.