diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 395f88351a..366972c165 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@master with: - dotnet-version: 3.1.100 + dotnet-version: 3.1.102 - name: Build All run: .\build-all.ps1 diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 4b7502f437..eef4237039 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -80,6 +80,8 @@ namespace Acme.BookStore.Permissions > ABP automatically discovers this class. No additional configuration required! +> You typically define this class inside the `Application.Contracts` project of your [application](Startup-Templates/Application.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with. + In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group. When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role: @@ -276,14 +278,24 @@ public async Task CreateAsync(CreateAuthorDto input) > Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check. -### Check a Permission in JavaScript +## Check a Permission in JavaScript + +You may need to check a policy/permission on the client side. + +### MVC UI -You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example: +For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. + +**Example: Check if a given permission has been granted for the current user** ```js abp.auth.isGranted('MyPermissionName'); ``` +### Angular UI + +See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI. + ## Permission Management Permission management is normally done by an admin user using the permission management modal: @@ -374,7 +386,7 @@ Configure(options => ### Permission Store -`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. Permission management module implements it. See the [permission management module documentation](Modules/Permission-Management.md) for more information +`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](Modules/Permission-Management.md) for more information ### AlwaysAllowAuthorizationService diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 36f3b8e105..ff2daad528 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -31,6 +31,7 @@ Here, the list of all available commands before explaining their details: * **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution. * **`add-package`**: Adds an ABP package to a project. * **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution. +* **`get-source`**: Downloads the source code of a module. * **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints. * **`remove-proxy`**: Removes previously generated client side proxies. * **`switch-to-preview`**: Switches to the latest preview version of the ABP Framework. @@ -178,6 +179,31 @@ abp add-module Volo.Blogging * `-sp` or `--startup-project`: Relative path to the project folder of the startup project. Default value is the current folder. * `--with-source-code`: Add source code of the module instead of NuGet/NPM packages. +### get-source + +Downloads the source code of a module to your computer. + +Usage + +````bash +abp get-source [options] +```` + +Example: + +```bash +abp get-source Volo.Blogging + +abp get-source Volo.Blogging --local-framework-ref --abp-path D:\GitHub\abp +``` + +#### Options + +* `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. +* `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. +* `--preview`: If no version option is specified, this option specifies if latest [preview version](Previews.md) will be used instead of latest stable version. +* `-- local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. + ### generate-proxy Generates Angular service proxies for your HTTP APIs to make easy to consume your services from the client side. Your host (server) application must be up and running before running this command. diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 85f094b614..62021452bd 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -151,11 +151,21 @@ public class Book : AggregateRoot, IIsActive ABP uses [EF Core's Global Query Filters](https://docs.microsoft.com/en-us/ef/core/querying/filters) system for the [EF Core Integration](Entity-Framework-Core.md). So, it is well integrated to EF Core and works as expected even if you directly work with `DbContext`. -Best way to implement a custom filter is to override `CreateFilterExpression` method for your `DbContext`. Example: +Best way to implement a custom filter is to override `ShouldFilterEntity` and `CreateFilterExpression` method for your `DbContext`. Example: ````csharp protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; +protected override bool ShouldFilterEntity(IMutableEntityType entityType) +{ + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return base.ShouldFilterEntity(entityType); +} + protected override Expression> CreateFilterExpression() { var expression = base.CreateFilterExpression(); @@ -174,7 +184,7 @@ protected override Expression> CreateFilterExpression The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). + +## 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** + +```csharp +public class ReportingAppService : ApplicationService, IReportingAppService +{ + [RequiresFeature("MyApp.PdfReporting")] + public async Task GetPdfReportAsync() + { + //TODO... + } +} +``` + +* `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) 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](Dependency-Injection.md). + +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](Dynamic-Proxying-Interceptors.md) system can not work. +* Only `async` methods (methods returning a `Task` or `Task`) 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** + +```csharp +public class ReportingAppService : ApplicationService, IReportingAppService +{ + private readonly IFeatureChecker _featureChecker; + + public ReportingAppService(IFeatureChecker featureChecker) + { + _featureChecker = featureChecker; + } + + public async Task 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** + +```csharp +public class ProductController : AbpController +{ + private readonly IFeatureChecker _featureChecker; + + public ProductController(IFeatureChecker featureChecker) + { + _featureChecker = featureChecker; + } + + public async Task 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 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: + +```csharp +var maxProductCountLimit = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +#### Extension Methods + +There are some useful extension methods for the `IFeatureChecker` interface; + +* `Task GetAsync(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** + +```csharp +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: + +```csharp +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("PdfReporting"), + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + displayName: LocalizableString + .Create("MaxProductCount"), + valueType: new FreeTextStringValueType( + new NumericValueValidator(0, 1000000)) + ); + } + } +} +``` + +* `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) 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: + +````json +"PdfReporting": "PDF Reporting", +"MaxProductCount": "Maximum number of products" +```` + +See the [localization document](Localization.md) for details about the localization system. + +### Feature Management Modal + +The [application startup template](Startup-Templates/Application.md) comes with the [tenant management](Modules/Tenant-Management.md) and the [feature management](Modules/Feature-Management.md) 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](images/features-action.png) + +This action opens a modal to manage the feature values for the selected tenant: + +![features-modal](images/features-modal.png) + +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** + +```csharp +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("Reporting"), + valueType: new ToggleStringValueType() + ); + + reportingFeature.CreateChild( + "MyApp.PdfReporting", + defaultValue: "false", + displayName: LocalizableString + .Create("PdfReporting"), + valueType: new ToggleStringValueType() + ); + + reportingFeature.CreateChild( + "MyApp.ExcelReporting", + defaultValue: "false", + displayName: LocalizableString + .Create("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](Module-Development-Basics.md)) and change their definitions. + +**Example: Manipulate an existing feature definition** + +```csharp +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](API/Application-Configuration.md) 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** + +````js +var isEnabled = abp.features.values["MyApp.ExcelReporting"] === "true"; +var count = abp.features.values["MyApp.MaxProductCount"]; +```` + +### Angular UI + +See the [features](Features.md) document for the Angular UI. + +## Feature Management + +Feature management is normally done by an admin user using the feature management modal: + +![features-modal](images/features-modal.png) + +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** + +```csharp +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](Modules/Feature-Management.md) 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 as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](Modules/Tenant-Management.md) module. You can implement it yourself or consider to use the [SaaS module](https://commercial.abp.io/modules/Volo.Saas) 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** + +```csharp +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 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: + +```csharp +Configure(options => +{ + options.ValueProviders.Add(); +}); +``` + +Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) 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](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information \ No newline at end of file diff --git a/docs/en/Index.md b/docs/en/Index.md index 5fe44625ad..949c04eb24 100644 --- a/docs/en/Index.md +++ b/docs/en/Index.md @@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation. ## Getting Started -The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) tutorial. +The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide. -Then you can continue with the [web application development tutorial](Tutorials/Part-1.md). +## Tutorials / Articles + +### Web Application Development + +[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework. + +### ABP Community Articles + +See also the [ABP Community](https://community.abp.io/) articles. + +## Samples + +See the [sample projects](Samples/Index.md) built with the ABP Framework. ## Source Code diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md index 5c41277dd8..968b742092 100644 --- a/docs/en/Modules/Feature-Management.md +++ b/docs/en/Modules/Feature-Management.md @@ -1,3 +1,5 @@ # Feature Management Module +> This module implements the `IFeatureStore` to store and manage feature values in a database. See the [Features System document](../Features.md) to understand the features first. + TODO \ No newline at end of file diff --git a/docs/en/Modules/Permission-Management.md b/docs/en/Modules/Permission-Management.md index faa03eecc9..ad48b60171 100644 --- a/docs/en/Modules/Permission-Management.md +++ b/docs/en/Modules/Permission-Management.md @@ -1,3 +1,5 @@ # Permission Management Module +This module implements the `IPermissionStore` to store and manage feature values in a database. See the [Authorization document](../Authorization.md) to understand the authorization and permission systems first. + TODO \ No newline at end of file diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 901a5c5cff..5138c9de04 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.com"); options.AddDomainTenantResolver("{0}.com"); ```` +## See Also + +* [Features](Features.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index fd34fcbae7..577129e139 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -8,27 +8,22 @@ The reason that you **can replace** but **cannot customize** default ABP compone Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. -Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: +Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below: ```js -import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action +import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum -import { Store } from '@ngxs/store'; // imported Store //... @Component(/* component metadata */) export class AppComponent { constructor( - private store: Store // injected Store - ) - { - // dispatched the AddReplaceableComponent action - this.store.dispatch( - new AddReplaceableComponent({ - component: YourNewRoleComponent, - key: eIdentityComponents.Roles, - }), - ); + private replaceableComponents: ReplaceableComponentsService, // injected the service + ) { + this.replaceableComponents.add({ + component: YourNewRoleComponent, + key: eIdentityComponents.Roles, + }); } } ``` @@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys -import { Store } from '@ngxs/store'; // imported Store import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent @Component(/* component metadata */) export class AppComponent { constructor( - private store: Store, // injected Store + private replaceableComponents: ReplaceableComponentsService, // injected the service ) { - // dispatched the AddReplaceableComponent action - this.store.dispatch( - new AddReplaceableComponent({ - component: MyApplicationLayoutComponent, - key: eThemeBasicComponents.ApplicationLayout, - }), - ); + this.replaceableComponents.add({ + component: MyApplicationLayoutComponent, + key: eThemeBasicComponents.ApplicationLayout, + }); } } ``` -> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. +> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. ### Layout Components @@ -123,26 +114,22 @@ export class LogoComponent {} Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store +import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents //... @Component(/* component metadata */) export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store + constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService ngOnInit() { //... - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ + this.replaceableComponents.add({ component: LogoComponent, key: eThemeBasicComponents.Logo, - }), - ); + }); } } ``` @@ -158,15 +145,13 @@ The final UI looks like below: Run the following command in `angular` folder to create a new component called `RoutesComponent`. ```bash -yarn ng generate component routes --entryComponent - -# You don't need the --entryComponent option in Angular 9 +yarn ng generate component routes ``` Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following: ```js -import { ABP, ReplaceableComponents } from '@abp/ng.core'; +import { ABP } from '@abp/ng.core'; import { Component, HostBinding, @@ -200,6 +185,21 @@ export class RoutesComponent implements AfterViewInit { } ``` +Import the `SharedModule` to the `imports` array of `AppModule`: + +```js +// app.module.ts + +import { SharedModule } from './shared/shared.module'; + +@NgModule({ + imports: [ + //... + SharedModule + ] +)} +``` + Open the generated `routes.component.html` in `src/app/routes` folder and replace its content with the following: ```html @@ -309,26 +309,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store +import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents //... @Component(/* component metadata */) export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store + constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService ngOnInit() { //... - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ + this.replaceableComponents.add({ component: RoutesComponent, key: eThemeBasicComponents.Routes, - }), - ); + }); } } ``` @@ -344,9 +340,7 @@ The final UI looks like below: Run the following command in `angular` folder to create a new component called `NavItemsComponent`. ```bash -yarn ng generate component nav-items --entryComponent - -# You don't need the --entryComponent option in Angular 9 +yarn ng generate component nav-items ``` Open the generated `nav-items.component.ts` in `src/app/nav-items` folder and replace the content with the following: @@ -431,6 +425,21 @@ export class NavItemsComponent implements AfterViewInit { } ``` +Import the `SharedModule` to the `imports` array of `AppModule`: + +```js +// app.module.ts + +import { SharedModule } from './shared/shared.module'; + +@NgModule({ + imports: [ + //... + SharedModule + ] +)} +``` + Open the generated `nav-items.component.html` in `src/app/nav-items` folder and replace the content with the following: ```html @@ -511,26 +520,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent -import { Store } from '@ngxs/store'; // imported Store +import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents //... @Component(/* component metadata */) export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store + constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService ngOnInit() { //... - // added dispatch - this.store.dispatch( - new AddReplaceableComponent({ + this.replaceableComponents.add({ component: NavItemsComponent, key: eThemeBasicComponents.NavItems, - }), - ); + }); } } ``` diff --git a/docs/en/UI/Angular/Features.md b/docs/en/UI/Angular/Features.md new file mode 100644 index 0000000000..f749030a26 --- /dev/null +++ b/docs/en/UI/Angular/Features.md @@ -0,0 +1,3 @@ +# Angular UI: Features + +> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system. \ No newline at end of file diff --git a/docs/en/UI/Angular/Permission-Management-Component-Replacement.md b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md index acdfffb948..9ae712cedd 100644 --- a/docs/en/UI/Angular/Permission-Management-Component-Replacement.md +++ b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md @@ -473,24 +473,20 @@ Open the generated `permission-management.component.html` in `src/app/permission Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { AddReplaceableComponent } from '@abp/ng.core'; +import { ReplaceableComponentsService } from '@abp/ng.core'; import { ePermissionManagementComponents } from '@abp/ng.permission-management'; import { Component, OnInit } from '@angular/core'; -import { Store } from '@ngxs/store'; import { PermissionManagementComponent } from './permission-management/permission-management.component'; //... export class AppComponent implements OnInit { - constructor(private store: Store) {} // injected store + constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService ngOnInit() { - // added dispatching the AddReplaceableComponent action - this.store.dispatch( - new AddReplaceableComponent({ + this.replaceableComponents.add({ component: PermissionManagementComponent, key: ePermissionManagementComponents.PermissionManagement, - }) - ); + }); } } ``` diff --git a/docs/en/UI/Angular/Service-Proxies.md b/docs/en/UI/Angular/Service-Proxies.md index 5466d64182..7dfb2c052c 100644 --- a/docs/en/UI/Angular/Service-Proxies.md +++ b/docs/en/UI/Angular/Service-Proxies.md @@ -1,47 +1,94 @@ ## Service Proxies -> THIS DOCUMENT IS OUTDATED. IT IS BEING UPDATED. MEANWHILE, YOU CAN [SEE THIS ARTICLE](https://github.com/abpframework/abp/blob/dev/docs/en/Blog-Posts/2020-09-07%20Angular-Service-Proxies/POST.md) TO LEARN HOW TO USE THE ABP ANGULAR SERVICE PROXIES. - It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on the server side) and **model objects** (matches to [DTOs](../../Data-Transfer-Objects) in the server side). In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced: -* It generates a **big, single** .ts file which has some problems; - * It get **too large** when your application grows. - * It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework. -* It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). -* It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. +- It generates a **big, single** .ts file which has some problems; + - It get **too large** when your application grows. + - It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework. +- It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). +- It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. -ABP CLI `generate-proxies` command automatically generates the typescript client proxies by creating folders which separated by module names in the `src/app` folder. +ABP CLI changes that via the `generate-proxy` command. It automatically generates the client proxies in TypeScript. by creating folders which separated by module names in the `src/app` folder. Run the following command in the **root folder** of the angular application: ```bash abp generate-proxy ``` -It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](../../CLI). +The command without any parameters creates proxies only for your own application's services and places them in your default Angular application. There are several parameters you may use to modify this behavior. See the [CLI documentation](../../CLI) for details. -The files generated with the `--module all` option like below: +The generated files will be placed in a folder called `proxy` at the root of target project. ![generated-files-via-generate-proxy](./images/generated-files-via-generate-proxy.png) +Each folder will have models, enums, and services defined at related namespace accompanied by a barrel export, i.e. an `index.ts` file for easier imports. + +> The cammand is able to find application/library roots by reading `angular.json` file. Make sure you have either defined your target project as the `defaultProject` or pass the `--target` parameter to the command. This also means that you may have a monorepo workspace. + +### Angular Project Configuration + +> If you've created your project with version 3.1 or later, you can skip this part since it will be already installed in your solution. + +For a solution that was created before v3.1, follow the steps below to configure your Angular application: + +1. Add `@abp/ng.schematics` package to the `devDependencies` of the Angular project. Run the following command in the root folder of the angular application: + +```bash +npm install @abp/ng.schematics -D +``` + +2. Add `rootNamespace` property to the `/src/environments/environment.ts` in your application project as shown below. `MyCompanyName.MyProjectName` should be replaced by the root namespace of your .NET project. + +```js +export const environment: Config.Environment = { + // other environment variables... + apis: { + default: { + rootNamespace: "MyCompanyName.MyProjectName", + // other environment variables... + }, + }, +}; +``` + +3. [OPTIONAL] Add the following paths to `tsconfig.base.json` in order to have a shortcut for importing proxies: + +```json +{ + // other TS configuration... + "compilerOptions": { + // other TS configuration... + "paths": { + "@proxy": ["src/app/proxy/index.ts"], + "@proxy/*": ["src/app/proxy/*"] + } + } +} +``` + +> The destination the `proxy` folder is created and the paths above may change based on your project structure. + ### Services -Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests#restservice). +The `generate-proxy` command generates one service per back-end controller and a method (property with a function value actually) for each action in the controller. These methods call backend APIs via [RestService](./Http-Requests#restservice). A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config) -The `providedIn` property of the services is defined as `'root'`. Therefore no need to add a service as a provider to a module. You can use a service by injecting it into a constructor as shown below: +The `providedIn` property of the services is defined as `'root'`. Therefore there is no need to provide them in a module. You can use them directly by injecting them into constructor as shown below: ```js -import { AbpApplicationConfigurationService } from '../abp/applicationconfiguration/services'; +import { BookService } from '@proxy/books'; -//... -export class HomeComponent{ - constructor(private appConfigService: AbpApplicationConfigurationService) {} +@Component(/* component metadata here */) +export class BookComponent implements OnInit { + constructor(private service: BookService) {} ngOnInit() { - this.appConfigService.get().subscribe() + this.service.get().subscribe( + // do something with the response + ); } } ``` @@ -50,20 +97,48 @@ The Angular compiler removes the services that have not been injected anywhere f ### Models -The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/models` folder. +The `generate-proxy` command generates interfaces matching DTOs in the back-end. There are also a few [core DTOs](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts) in the `@abp/ng.core` package. In combination, these models can be used to reflect the APIs. + +```js +import { PagedResultDto } from "@abp/ng.core"; +import { BookDto } from "@proxy/books"; + +@Component(/* component metadata here */) +export class BookComponent implements OnInit { + data: PagedResultDto = { + items: [], + totalCount: 0, + }; +} +``` -There are a few [base classes](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts) in the `@abp/ng.core` package. Some models extend these classes. +### Enums -A class instance can be created as shown below: +Enums have always been difficult to populate in the frontend. The `generate-proxy` command genarates enums in a separate file and exports a ready-to-use options constant from the same file. So you can import them as follows: ```js -import { IdentityRoleCreateDto } from '../identity/role/models' -//... -const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true}) +import { bookGenreOptions } from "@proxy/books"; + +@Component(/* component metadata here */) +export class BookComponent implements OnInit { + genres = bookGenreOptions; +} +``` + +...and consume the options in the template as follows: + +```html + + ``` -Initial values ​​can optionally be passed to each class constructor. +> Please [see this article](https://github.com/abpframework/abp/blob/dev/docs/en/Blog-Posts/2020-09-07%20Angular-Service-Proxies/POST.md) to learn more about service proxies. ## What's Next? -* [HTTP Requests](./Http-Requests) +- [HTTP Requests](./Http-Requests) diff --git a/docs/en/UI/Angular/images/generated-files-via-generate-proxy.png b/docs/en/UI/Angular/images/generated-files-via-generate-proxy.png index 0d062cc2d3..a52c4bcabb 100644 Binary files a/docs/en/UI/Angular/images/generated-files-via-generate-proxy.png and b/docs/en/UI/Angular/images/generated-files-via-generate-proxy.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 1e6e02855f..0273030308 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -170,6 +170,10 @@ "text": "Settings", "path": "Settings.md" }, + { + "text": "Features", + "path": "Features.md" + }, { "text": "Data Filtering", "path": "Data-Filtering.md" diff --git a/docs/en/images/features-action.png b/docs/en/images/features-action.png new file mode 100644 index 0000000000..8283a95be9 Binary files /dev/null and b/docs/en/images/features-action.png differ diff --git a/docs/en/images/features-modal.png b/docs/en/images/features-modal.png new file mode 100644 index 0000000000..74ee8aabc3 Binary files /dev/null and b/docs/en/images/features-modal.png differ diff --git a/docs/zh-Hans/Data-Filtering.md b/docs/zh-Hans/Data-Filtering.md index 548f70c80c..45adcb6b05 100644 --- a/docs/zh-Hans/Data-Filtering.md +++ b/docs/zh-Hans/Data-Filtering.md @@ -151,11 +151,21 @@ public class Book : AggregateRoot, IIsActive ABP使用[EF Core的全局过滤](https://docs.microsoft.com/en-us/ef/core/querying/filters)系统用于[EF Core 集成](Entity-Framework-Core.md). 所以它很好的集成到EF Core中,即使你直接使用 `DbContext` 它也可以正常工作. -实现自定义过滤的最佳方法是为重写你的 `DbContext` 的 `CreateFilterExpression` 方法. 示例: +实现自定义过滤的最佳方法是为重写你的 `DbContext` 的 `ShouldFilterEntity` 和 `CreateFilterExpression` 方法. 示例: ````csharp protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; +protected override bool ShouldFilterEntity(IMutableEntityType entityType) +{ + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return base.ShouldFilterEntity(entityType); +} + protected override Expression> CreateFilterExpression() { var expression = base.CreateFilterExpression(); @@ -174,7 +184,7 @@ protected override Expression> CreateFilterExpression + + + +```` + +此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件). + 如果文件名包含一些特殊字符,在项目/程序集中嵌入文件可能会导致问题. 为了克服这个限制; 1. 将[Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet包添加到包含嵌入式资源的项目中. 2. 添加 `true` 到 `.csproj` 文件的 `...` 部分中. -此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件). - > 尽管这两个步骤是可选的,并且ABP无需这些配置即可工作,但强烈建议你这样做. ### 配置AbpVirtualFileSystemOptions diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs index ce08bebe87..5d1f6d8414 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs @@ -13,8 +13,18 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public string UserName { get; set; } + public string Name { get; set; } + + public string SurName { get; set; } + public string Email { get; set; } + public bool EmailVerified { get; set; } + + public string PhoneNumber { get; set; } + + public bool PhoneNumberVerified { get; set; } + public string[] Roles { get; set; } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs index 722460992d..1f1f71b066 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs @@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S output.PreContent.AppendHtml($"

{Title}

"); output.PreContent.AppendHtml("
"); output.PreContent.AppendHtml("