Skip to content

Latest commit

 

History

History
448 lines (323 loc) · 18.3 KB

Features.md

File metadata and controls

448 lines (323 loc) · 18.3 KB

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. Use RequiresAll 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 check 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 Framework 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 be virtual. Otherwise, dynamic proxy / interception system can not work.
  • Only async methods (methods returning a Task or Task<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 Framework 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 type T. Allows to specify a defaultValue that is returned when the feature value is null.
  • CheckEnabledAsync(string name): Checks if given feature is enabled. Throws an AbpAuthorizationException if the feature was not true (enabled).

Defining the Features

A feature should be defined to be able to check it.

FeatureDefinitionProvider

Create a class inheriting the FeatureDefinitionProvider to define permissions.

Example: Defining permissions

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 a boolean feature with false as the default value.
  • Second feature, named MyApp.MaxProductCount, is a numeric feature with 10 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 the IStringValueType. Built-in types:
    • ToggleStringValueType: Used to define true/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 to FreeTextStringValueType with a numeric validator that allows to the values from 0 to 1,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):

features-action

This action opens a modal to manage the feature values for the selected tenant:

features-modal

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 permission 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.

ASP.NET Core MVC / Razor Pages UI

Use abp.features API to get the feature values.

Example: Get feature values in the JavaScript code

var isEnabled = abp.features.values["MyApp.ExcelReporting"] === "true";
var count = abp.features.values["MyApp.MaxProductCount"];

Angular UI

See the features document for the Angular UI.

Feature Management

Feature management is normally done by an admin user using the feature management modal:

features-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 name editionid (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