-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeature-management.html
241 lines (226 loc) · 11.3 KB
/
feature-management.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>ASP.NET Boilerplate</title>
<link type="text/css" rel="stylesheet" href="bootstrap.min.css" />
</head>
<body>
<div class="document-contents">
<h3>Introduction</h3>
<p>Most <strong>SaaS</strong> (multi-tenant) applications have <strong>editions
</strong>(packages) those have different <strong>features</strong>. Thus, they
can provide different <strong>price and feature options</strong> to thier
tenants (customers).</p>
<p>ASP.NET Boilerplate provides a <strong>feature system</strong> to make
it easier. We can <strong>define</strong> features, <strong>check </strong>
if a feature is <strong>enabled</strong> for a tenant and <strong>integrate
</strong>feature system to other ASP.NET Boilerplate concepts (like permissions
and menus).</p>
<div class="bs-callout bs-callout-warning">
<h4>About IFeatureValueStore</h4>
<p>Feature system uses <strong>IFeatureValueStore</strong> to
get values of features. While you can
implement it in your own way, it's fully implemented in <strong>module-zero</strong>
project. If it's not implemented, NullFeatureValueStore is used which
returns null for all features (Default feature values are used in this
case).</p>
</div>
<h3>Feature Types</h3>
<p>There are two fundamental feature types.</p>
<h4>Boolean Feature</h4>
<p>Can be "true" or "false". This type of a feature can be <strong>enabled</strong>
or <strong>disabled</strong> (for an edition or for a tenant).</p>
<h4>Value Feature</h4>
<p>Can be an <strong>arbitrary value</strong>. While it's stored and
retrieved a string, numbers also can be easily stored as strings.</p>
<p>For example,
our application may be a task management application and we may have a limit for
creating tasks in a month. Say that we have two different edition/package; one allows
creating 1,000 tasks per month, while other allows creating 5,000
tasks per month. So, this feature should be stored as value, not simply
true/false.</p>
<h3>Defining Features</h3>
<p>A feature should be defined before checking. A
<a href="/Pages/Documents/Module-System">module</a> can define it's own
features by deriving from <strong>FeatureProvider</strong> class. Here, a
very simple feature provider that defines 3 features:</p>
<pre lang="cs">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");
}
}</pre>
<p>After creating a feature provider, we should register it in our module's
<a href="/Pages/Documents/Module-System#DocModulePreInit">PreInitialize</a>
method as shown below:</p>
<pre lang="cs">Configuration.Features.Providers.Add<AppFeatureProvider>();</pre>
<h4>Basic Feature Properties</h4>
<p>A feature definition requires two properties at minimum:</p>
<ul>
<li><strong>Name</strong>: A unique name (as string) to identify the
feature.</li>
<li><strong>Default value</strong>: A default value. This is used when
we need the value of the feature and it's not available for current
tenant.</li>
</ul>
<p>Here, we defined a boolean feature named "SampleBooleanFeature" which's
default value is "false" (not enabled). We also defined two value features
(SampleNumericFeature is defined as child of SampleBooleanFeature).</p>
<p>Tip: Create a const string for a feature name and use it everywhere to
prevent typing errors.</p>
<h4>Other Feature Properties</h4>
<p>While unique name and default value properties are enough for ASP.NET
Boilerplate, there are somre other feature properties for a detailed
control.</p>
<ul>
<li><strong>Scope</strong>: A value in FeatureScopes enum. It can be
<strong>Edition</strong> (if this feature can be set only for edition
level), <strong>Tenant</strong> (if this feature can be set only for
tenant level) or <strong>All</strong> (if this feature can be set for
editions and tenants, where tenant setting overrides it's edition's
setting). Default value is <strong>All</strong>.</li>
<li><strong>DisplayName</strong>: A localizable string to show the
feature's name to users.</li>
<li><strong>Description</strong>: A localizable string to show the
feature's detailed description to users.</li>
<li><strong>InputType</strong>: A UI input type for the feature. This
can be defined, then can be used while creating an automatic feature
screen.</li>
<li><strong>Attributes</strong>: An arbitrary custom dictionary of
key-value pairs those can be related to the feature.</li>
</ul>
<p>Let's see more detailed definitions for the features above:</p>
<pre lang="cs">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);
}
}</pre>
<p>Note that; This input type definitions are not used by ASP.NET
Boilerplate. They can be used by applications while creating inputs for
features. ASP.NET Boilerplate just provides these options to make it easier.</p>
<h4>Feature Hierarchy</h4>
<p>As shown in the sample feature providers, a feature can have <strong>
child features</strong>. A Parent feature is generally defined as <strong>
boolean</strong> feature. Child features will be available only if the
parent enabled. ASP.NET Boilerplate <strong>does not</strong> enforces but suggests this. Applications should
take care of it.</p>
<h3>Checking Features</h3>
<p>We define a feature to check it's value in the application to allow or
block some application features per tenant. There are different ways of
checking it.</p>
<h4>Using RequiresFeature Attribute</h4>
<p>We can use <strong>RequiredFeature</strong> attribute for a method or a
class as shown below:</p>
<pre>[RequiresFeature("ExportToExcel")]
public async Task<FileDto> GetReportToExcel(...)
{
...
}</pre>
<p>This method is executed only if "ExportToExcel" feature is enabled for
the <strong>current tenant</strong> (current tenant is obtained from
<a href="/Pages/Documents/Abp-Session">IAbpSession</a>). If it's not enabled, an
<strong>AbpAuthorizationException</strong> is thrown
automatically.</p>
<p>Surely, RequiresFeature attribute should be used for <strong>boolean type
features</strong>. Otherwise, you may get exceptions.</p>
<h5>RequiresFeature attribute notes</h5>
<p>ASP.NET Boilerplate uses power of dynamic method interception for
feature checking. So, there is some restrictions for the methods use
RequiresFeature
attribute.</p>
<ul>
<li>Can not use it for private methods.</li>
<li>Can not use it for static methods.</li>
<li>Can not use it for methods of a non-injected class (We must use
<a href="/Pages/Documents/Dependency-Injection">dependency injection</a>).</li>
</ul>
<p>Also,</p>
<ul>
<li>Can use it for any <strong>public</strong> method if the method is called over an
<strong>interface </strong>(like Application Services used over interface).</li>
<li>A method should be <strong>virtual</strong> if it's called directly
from class reference (like ASP.NET MVC or Web API Controllers).</li>
<li>A method should be <strong>virtual</strong> if it's <strong>protected</strong>.</li>
</ul>
<h4>Using IFeatureChecker</h4>
<p>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).</p>
<h5>IsEnabled</h5>
<p>Used to simply check if given feature is enabled or not. Example:</p>
<pre lang="cs">public async Task<FileDto> GetReportToExcel(...)
{
if (await FeatureChecker.IsEnabledAsync("ExportToExcel"))
{
throw new AbpAuthorizationException("You don't have this feature: ExportToExcel");
}
...
}</pre>
<p>IsEnabledAsync and other methods have sync versions.</p>
<p>Surely, IsEnabled
method should be used for <strong>boolean type
features</strong>. Otherwise, you may get exceptions.</p>
<p>If you just want to check a feature and throw exception as shown in the
example, you can just use <strong>CheckEnabled</strong> method.</p>
<h5>GetValue</h5>
<p>Used to get current value of a feature for value type features. Example:</p>
<pre lang="cs">var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth();
if (createdTaskCountInThisMonth >= <strong>FeatureChecker.GetValue("MaxTaskCreationLimitPerMonth").To<int>()</strong>)
{
throw new AbpAuthorizationException("You exceed task creation limit for this month, sorry :(");
}</pre>
<p>FeatureChecker methods has also overrides to work features for a
specified tenantId, not only current tenantId.</p>
<h4>Client Side</h4>
<p>In the client side, we can use <strong>abp.features</strong> namespace to
get current values of the features.</p>
<h5>isEnabled</h5>
<pre lang="js">var isEnabled = abp.features.isEnabled('SampleBooleanFeature');</pre>
<h5>getValue</h5>
<pre lang="js">var value = abp.features.getValue('SampleNumericFeature');</pre>
<h3>Feature Manager</h3>
<p>If you need to definitions of features, you can inject and use <strong>
IFeatureManager</strong>.</p>
<h3>A Note For Editions</h3>
<p>ASP.NET Boilerplate framework has not a built-in edition system because such
a system requires a database (to store editions, edition features,
tenant-edition mappings and so on...). Therefore, edition system is
implemented in <a href="/Pages/Documents/Zero/Edition-Management">module
zero</a>. You can use it to easily have an edition system, or you can
implement all yourself.</p>
</div>
</body>
</html>