Features
ABP Feature system is used to enable, disable or change the behavior of the application features on runtime.
The runtime value for a feature is generally a boolean
value, like true
(enabled) or false
(disabled). However, you can get/set any kind of value for feature.
Feature system was originally designed to control the tenant features in a multi-tenant application. However, it is extensible and capable of determining the features by any condition.
The feature system is implemented with the Volo.Abp.Features NuGet package. Most of the times you don't need to manually install it since it comes pre-installed with the application startup template.
Checking for the Features
Before explaining to define features, let's see how to check a feature value in your application code.
RequiresFeature Attribute
[RequiresFeature]
attribute (defined in the Volo.Abp.Features
namespace) is used to declaratively check if a feature is true
(enabled) or not. It is a useful shortcut for the boolean
features.
Example: Check if the "PDF Reporting" feature enabled
public class ReportingAppService : ApplicationService, IReportingAppService
{
[RequiresFeature("MyApp.PdfReporting")]
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
//TODO...
}
}
RequiresFeature(...)
simply gets a feature name to check if it is enabled or not. If not enabled, an authorization exception is thrown and a proper response is returned to the client side.[RequiresFeature]
can be used for a method or a class. When you use it for a class, all the methods of that class require the given feature.RequiresFeature
may get multiple feature names, like[RequiresFeature("Feature1", "Feature2")]
. In this case ABP checks if any of the features enabled. UseRequiresAll
option, like[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]
to force to check all of the features to be enabled.- Multiple usage of
[RequiresFeature]
attribute is supported for a method or class. ABP checks all of them in that case.
Feature name can be any arbitrary string. It should be unique for a feature.
About the Interception
ABP uses the interception system to make the [RequiresFeature]
attribute working. So, it can work with any class (application services, controllers...) that is injected from the dependency injection.
However, there are some rules should be followed in order to make it working;
- If you are not injecting the service over an interface (like
IMyService
), then the methods of the service must bevirtual
. Otherwise, dynamic proxy / interception system can not work. - Only
async
methods (methods returning aTask
orTask<T>
) are intercepted.
There is an exception for the controller and razor page methods. They don't require the following the rules above, since ABP uses the action/page filters to implement the feature checking in this case.
IFeatureChecker Service
IFeatureChecker
allows to check a feature in your application code.
IsEnabledAsync
Returns true
if the given feature is enabled. So, you can conditionally execute your business flow.
Example: Check if the "PDF Reporting" feature enabled
public class ReportingAppService : ApplicationService, IReportingAppService
{
private readonly IFeatureChecker _featureChecker;
public ReportingAppService(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting"))
{
//TODO...
}
else
{
//TODO...
}
}
}
IsEnabledAsync
has overloads to check multiple features in one method call.
GetOrNullAsync
Gets the current value for a feature. This method returns a string
, so you store any kind of value inside it, by converting to or from string
.
Example: Check the maximum product count allowed
public class ProductController : AbpController
{
private readonly IFeatureChecker _featureChecker;
public ProductController(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<IActionResult> Create(CreateProductModel model)
{
var currentProductCount = await GetCurrentProductCountFromDatabase();
//GET THE FEATURE VALUE
var maxProductCountLimit =
await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount");
if (currentProductCount >= Convert.ToInt32(maxProductCountLimit))
{
throw new BusinessException(
"MyApp:ReachToMaxProductCountLimit",
$"You can not create more than {maxProductCountLimit} products!"
);
}
//TODO: Create the product in the database...
}
private async Task<int> GetCurrentProductCountFromDatabase()
{
throw new System.NotImplementedException();
}
}
This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application.
Instead of manually converting the value to int
, you can use the generic overload of the GetAsync
method:
var maxProductCountLimit = await _featureChecker.GetAsync<int>("MyApp.MaxProductCount");
Extension Methods
There are some useful extension methods for the IFeatureChecker
interface;
Task<T> GetAsync<T>(string name, T defaultValue = default)
: Used to get a value of a feature with the given typeT
. Allows to specify adefaultValue
that is returned when the feature value isnull
.CheckEnabledAsync(string name)
: Checks if given feature is enabled. Throws anAbpAuthorizationException
if the feature was nottrue
(enabled).
Defining the Features
A feature should be defined to be able to check it.
FeatureDefinitionProvider
Create a class inheriting the FeatureDefinitionProvider
to define features.
Example: Defining features
using Volo.Abp.Features;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false");
myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10");
}
}
}
ABP automatically discovers this class and registers the features. No additional configuration required.
This class is generally created in the
Application.Contracts
project of your solution.
- In the
Define
method, you first need to add a feature group for your application/module or get an existing group then add features to this group. - First feature, named
MyApp.PdfReporting
, is aboolean
feature withfalse
as the default value. - Second feature, named
MyApp.MaxProductCount
, is a numeric feature with10
as the default value.
Default value is used if there is no other value set for the current user/tenant.
Other Feature Properties
While these minimal definitions are enough to make the feature system working, you can specify the optional properties for the features;
DisplayName
: A localizable string that will be used to show the feature name on the user interface.Description
: A longer localizable text to describe the feature.ValueType
: Type of the feature value. Can be a class implementing theIStringValueType
. Built-in types:ToggleStringValueType
: Used to definetrue
/false
,on
/off
,enabled
/disabled
style features. A checkbox is shown on the UI.FreeTextStringValueType
: Used to define free text values. A textbox is shown on the UI.SelectionStringValueType
: Used to force the value to be selected from a list. A dropdown list is shown on the UI.
IsVisibleToClients
(default:true
): Set false to hide the value of this feature from clients (browsers). Sharing the value with the clients helps them to conditionally show/hide/change the UI parts based on the feature value.Properties
: A dictionary to set/get arbitrary key-value pairs related to this feature. This can be a point for customization.
So, based on these descriptions, it would be better to define these features as shown below:
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
myGroup.AddFeature(
"MyApp.MaxProductCount",
defaultValue: "10",
displayName: LocalizableString
.Create<FeaturesDemoResource>("MaxProductCount"),
valueType: new FreeTextStringValueType(
new NumericValueValidator(0, 1000000))
);
}
}
}
FeaturesDemoResource
is the project name in this example code. See the localization document for details about the localization system.- First feature is set to
ToggleStringValueType
, while the second one is set toFreeTextStringValueType
with a numeric validator that allows to the values from0
to1,000,000
.
Remember to define the localization the keys in your localization file:
"PdfReporting": "PDF Reporting",
"MaxProductCount": "Maximum number of products"
See the localization document for details about the localization system.
Feature Management Modal
The application startup template comes with the tenant management and the feature management modules pre-installed.
Whenever you define a new feature, it will be available on the feature management modal. To open this modal, navigate to the tenant management page and select the Features
action for a tenant (create a new tenant if there is no tenant yet):
This action opens a modal to manage the feature values for the selected tenant:
So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application.
See the Feature Management section below to learn more about managing the features.
Child Features
A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled.
Example: Defining child features
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
var reportingFeature = myGroup.AddFeature(
"MyApp.Reporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("Reporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.ExcelReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("ExcelReporting"),
valueType: new ToggleStringValueType()
);
}
}
}
The example above defines a Reporting feature with two children: PDF Reporting and Excel Reporting.
Changing Features Definitions of a Depended Module
A class deriving from the FeatureDefinitionProvider
(just like the example above) can also get the existing feature definitions (defined by the depended modules) and change their definitions.
Example: Manipulate an existing feature definition
var someGroup = context.GetGroupOrNull("SomeModule");
var feature = someGroup.Features.FirstOrDefault(f => f.Name == "SomeFeature");
if (feature != null)
{
feature.Description = ...
feature.CreateChild(...);
}
Check a Feature in the Client Side
A feature value is available at the client side too, unless you set IsVisibleToClients
to false
on the feature definition. The feature values are exposed from the Application Configuration API and usable via some services on the UI.
See the following documents to learn how to check features in different UI types:
Blazor applications can use the same IFeatureChecker
service as explained above.
Feature Management
Feature management is normally done by an admin user using the feature management modal:
This modal is available on the related entities, like tenants in a multi-tenant application. To open it, navigate to the Tenant Management page (for a multi-tenant application), click to the Actions button left to the Tenant and select the Features action.
If you need to manage features by code, inject the IFeatureManager
service.
Example: Enable PDF reporting for a tenant
public class MyService : ITransientDependency
{
private readonly IFeatureManager _featureManager;
public MyService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task EnablePdfReporting(Guid tenantId)
{
await _featureManager.SetForTenantAsync(
tenantId,
"MyApp.PdfReporting",
true.ToString()
);
}
}
IFeatureManager
is defined by the Feature Management module. It comes pre-installed with the application startup template. See the feature management module documentation for more information.
Advanced Topics
Feature Value Providers
Feature system is extensible. Any class derived from FeatureValueProvider
(or implements IFeatureValueProvider
) can contribute to the feature system. A value provider is responsible to obtain the current value of a given feature.
Feature value providers are executed one by one. If one of them return a non-null value, then this feature value is used and the other providers are not executed.
There are three pre-defined value providers, executed by the given order:
TenantFeatureValueProvider
tries to get if the feature value is explicitly set for the current tenant.EditionFeatureValueProvider
tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (ICurrentPrincipalAccessor
) with the claim nameeditionid
(a constant defined asAbpClaimTypes.EditionId
). Editions are not implemented for the tenant management module. You can implement it yourself or consider to use the SaaS module of the ABP Commercial.DefaultValueFeatureValueProvider
gets the default value of the feature.
You can write your own provider by inheriting the FeatureValueProvider
.
Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value
using System.Threading.Tasks;
using Volo.Abp.Features;
using Volo.Abp.Security.Claims;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class SystemAdminFeatureValueProvider : FeatureValueProvider
{
public override string Name => "SA";
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;
public SystemAdminFeatureValueProvider(
IFeatureStore featureStore,
ICurrentPrincipalAccessor currentPrincipalAccessor)
: base(featureStore)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
}
public override Task<string> GetOrNullAsync(FeatureDefinition feature)
{
if (feature.ValueType is ToggleStringValueType &&
_currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin")
{
return Task.FromResult("true");
}
return null;
}
}
}
If a provider returns null
, then the next provider is executed.
Once a provider is defined, it should be added to the AbpFeatureOptions
as shown below:
Configure<AbpFeatureOptions>(options =>
{
options.ValueProviders.Add<SystemAdminFeatureValueProvider>();
});
Use this code inside the ConfigureServices
of your module class.
Feature Store
IFeatureStore
is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the feature management module documentation for more information