@ -0,0 +1,64 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Permission:CommunityArticle": "社区文章", |
|||
"Permission:Edit": "修改", |
|||
"Waiting": "等待中", |
|||
"Approved": "已批准", |
|||
"Rejected": "已拒绝", |
|||
"Wait": "等待", |
|||
"Approve": "批准", |
|||
"Reject": "拒绝", |
|||
"ReadArticle": "阅读文章", |
|||
"Status": "状态", |
|||
"ContentSource": "内容来源", |
|||
"Details": "详情", |
|||
"Url": "url", |
|||
"Title": "标题", |
|||
"CreationTime": "创建时间", |
|||
"Save": "保存", |
|||
"SameUrlAlreadyExist": "url已存在,如果你想要添加这篇文章,你需要更改url!", |
|||
"UrlIsNotValid": "Url无效.", |
|||
"UrlNotFound" : "Url未找到.", |
|||
"UrlContentNotFound": "Url内容未找到.", |
|||
"Summary": "摘要", |
|||
"MostRead": "阅读最多", |
|||
"Latest": "最新", |
|||
"ContributeAbpCommunity": "为ABP社区做贡献", |
|||
"SubmitYourArticle": "提交你的文章", |
|||
"ContributionGuide": "贡献指南", |
|||
"BugReport": "Bug报告", |
|||
"SeeAllArticles": "查看所有的文章", |
|||
"WelcomeToABPCommunity!": "欢迎来到ABP社区!", |
|||
"MyProfile": "我的资料", |
|||
"MyOrganizations": "我的组织", |
|||
"EmailNotValid": "请输入有效的电子邮箱地址.", |
|||
"FeatureRequest": "功能请求", |
|||
"CreateArticleTitleInfo": "文章标题显示在文章列表中.", |
|||
"CreateArticleUrlInfo": "文章的原始GitHub/外部URL.", |
|||
"CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.", |
|||
"CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!", |
|||
"ThisExtensionIsNotAllowed": "不允许此扩展名.", |
|||
"TheFileIsTooLarge": "文件过大.", |
|||
"GoToTheArticle": "转到文章", |
|||
"Contribute": "贡献", |
|||
"OverallProgress": "总体流程", |
|||
"Done": "完成", |
|||
"Open": "打开", |
|||
"Closed": "关闭", |
|||
"LatestQuestionOnThe": "有关的最新问题", |
|||
"Stackoverflow": "Stackoverflow", |
|||
"Votes": "票数", |
|||
"Answer": "回答", |
|||
"Views": "观看次数", |
|||
"Answered": "已回答", |
|||
"WaitingForYourAnswer": "等待你的回答", |
|||
"Asked": "提问", |
|||
"AllQuestions": "所有的问题", |
|||
"NextVersion": "下一个版本", |
|||
"MilestoneErrorMessage": "无法从Github获取当前的里程碑详细信息.", |
|||
"QuestionItemErrorMessage": "无法从Stackoverflow获取最新的问题详细信息.", |
|||
"Oops": "哎呀!" |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,247 @@ |
|||
# ABP Framework v3.1 RC.1 Has Been Released |
|||
|
|||
Today, we are releasing the **ABP Framework version 3.1 Release Candidate 1** (RC.1). The development cycle for this version was **~7 weeks**. It was the longest development cycle for a feature version release ever. We have completed **~150 issues**, merged **~150 PRs** and made **~1,000 commits** only in the main [abp repository](https://github.com/abpframework/abp). See the related [milestone](https://github.com/abpframework/abp/milestone/38?closed=1) on GitHub. |
|||
|
|||
There were two main reasons of this long development cycle; |
|||
|
|||
* We've switched to **4-weeks** release cycle (was discussed in [this issue](https://github.com/abpframework/abp/issues/4692)). |
|||
* We've [re-written](https://github.com/abpframework/abp/issues/4881) the Angular service proxy generation system using the Angular schematics to make it more stable. There were some problems with the previous implementation. |
|||
|
|||
This long development cycle brings a lot of new features, improvements and bug fixes. I will highlight the fundamental features and changes in this blog post. |
|||
|
|||
## About the Preview/Stable Version Cycle |
|||
|
|||
As mentioned above, it is planned to release a new stable feature version (like 3.1, 3.2, 3.3...) in every 4-weeks. |
|||
|
|||
In addition, we are starting to deploy a **preview version** 2-weeks before the stable versions for every feature/major releases. |
|||
|
|||
Today, we've released `3.1.0-rc.1` as the first preview version. We may release more previews if it is needed until the stable 3.1.0 version. |
|||
|
|||
**The stable `3.1.0` version will be released on September 3, 2020.** Next RC version, `3.2.0-rc.1`, is planned for September 17, 2020 (2 weeks after the stable 3.1.0 and 2 weeks before the stable 3.2.0). |
|||
|
|||
We **won't add new features** to a version after publishing the preview version. We only will make **bug fixes** until the stable version. The new features being developed in this period will be available in the next version. |
|||
|
|||
> We will use `-rc.x` suffix (like `3.1.0-rc.1` and `3.1.0-rc.2`) for preview releases. However, we may also publish with `-preview.x` suffix before RC (Release Candidate) releases, especially for major versions (like 4.x, 5.x...). |
|||
|
|||
### About the Nightly Builds |
|||
|
|||
Don't confuse preview versions vs nightly builds. When we say preview, we are mentioning the preview system explained above. |
|||
|
|||
We will continue to publish **nightly builds** for all the [ABP Framework packages](https://abp.io/packages). Nightly pages are built from the development branch. You can refer to [this document](https://docs.abp.io/en/abp/latest/Nightly-Builds) to learn how to use the nightly packages. |
|||
|
|||
## Get Started with the RC Versions |
|||
|
|||
Please try the preview versions and provide feedback to us to release more stable versions. Please open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new) if you find a bug or want to give feedback. |
|||
|
|||
### Update the ABP CLI to the 3.1.0-rc.1 |
|||
|
|||
Since this is the first preview version, you need to upgrade the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to the `3.1.0-rc.1` to be able to use the preview features: |
|||
|
|||
````bash |
|||
dotnet tool update Volo.Abp.Cli -g --version 3.1.0-rc.1 |
|||
```` |
|||
|
|||
### New Solutions |
|||
|
|||
The [ABP.IO](https://abp.io/) platform and the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) are compatible with the RC system. You can select the "preview" option on the [download page](https://abp.io/get-started) or use the "**--preview**" parameter with the ABP CLI [new](https://docs.abp.io/en/abp/latest/CLI?_ga=2.106435654.411298747.1597771169-1910388957.1594128976#new) command: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore --preview |
|||
```` |
|||
|
|||
This command will create a new project with the latest RC/Preview version. Whenever the stable version is released, you can switch to the stable version for your solution using the `abp switch-to-stable` command in the root folder of your solution. |
|||
|
|||
### Existing Solutions |
|||
|
|||
If you already have a solution and want to use/test the latest RC/Preview version, use the `abp switch-to-preview` command in the root folder of your solution. You can return back to the latest stable using the `abp switch-to-stable ` command later. |
|||
|
|||
> Note that the `abp switch-to-preview` command was being used to switch to nightly builds before the v3.1. Now, you should use the `abp switch-to-nightly` for [nightly builds](https://docs.abp.io/en/abp/latest/Nightly-Builds). |
|||
|
|||
## What's New with the ABP Framework 3.1 RC.1 |
|||
|
|||
### Angular Service Proxies |
|||
|
|||
ABP provides a system to generate Angular service proxies (with TypeScript) to consume the HTTP APIs of your application. Service proxy generation system **has been completely re-written** with the ABP Framework 3.1. The main goal was to build more stable and feature rich system that is better aligned with other ABP Framework features (like [modularity](https://docs.abp.io/en/abp/latest/Module-Development-Basics)). |
|||
|
|||
[See the documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Service-Proxies) to learn more about the service proxy generation for Angular applications. |
|||
|
|||
### Authorization Code Flow for the Angular UI |
|||
|
|||
We were using the **resource owner password authentication** flow for the Angular UI login page. We've implemented **Authorization Code Flow** for the Angular account module and made it **default for new projects**. With this change, the Angular application now redirects to the login page of the MVC UI which was implemented using the Identity Server 4. We also removed the client secret from the Angular side with this change. |
|||
|
|||
Old behavior remains exist. If you want to switch to the new flow (which is recommended), follow the steps below: |
|||
|
|||
1) Add `authorization_code` to the `IdentityServerClientGrantTypes` table in the database, for the client used by the Angular UI (the `ClientId` is `YourProjectName_App` by default, in the `IdentityServerClients` table). |
|||
|
|||
2) Add `http://localhost:4200` to `IdentityServerClientRedirectUris` and `IdentityServerClientPostLogoutRedirectUris` tables for the same client. |
|||
|
|||
3) Set `RequireClientSecret` to `false` in the `IdentityServerClients` table for the same client. |
|||
|
|||
> [ABP Commercial](https://commercial.abp.io/) users can make these changes on the [Identity Server Management UI](https://commercial.abp.io/modules/Volo.Identityserver.Ui). |
|||
|
|||
4) Change the `oAuthConfig` section in the `src/environments/environment.ts` file of the Angular application. |
|||
|
|||
You can take [this new configuration](https://gist.github.com/hikalkan/e7f6ae7f507b201783682dccaeadc5e3) as a reference. Main changes are; |
|||
|
|||
* Added `responseType` as `code`. |
|||
* Added `redirectUri` |
|||
* Added `offline_access` to the `scope`. |
|||
* Removed `oidc: false` option. |
|||
* Removed the client secret option. |
|||
|
|||
### Global Feature System |
|||
|
|||
The new "Global Features" system allows to **enable/disable features of an application or a module** in a central point. It is especially useful if you want to use a module but don't want to bring all its features into your application. If the module was so designed, you can enable only the features you need. |
|||
|
|||
When you disable a feature; |
|||
|
|||
* The **database tables** related to that feature should not be created in the database. |
|||
* The **HTTP APIs** related to that feature should not be exposed. They returns 404 if they are directly requested. |
|||
|
|||
So, the goal is that; when you disable a feature, it should behave like that feature doesn't exists in your system at all. |
|||
|
|||
There is **no way to enable/disable a global feature on runtime**. You should decide it in the development time (remember, even database tables are not being created for disabled global features, so you can't enable it on runtime). |
|||
|
|||
> "Global Features" system is different than [SaaS/multi-tenancy features](https://docs.abp.io/en/abp/latest/Features), where you can enable/disable features for your tenants on runtime. |
|||
|
|||
Assume that you are using the [CMS Kit module](https://github.com/abpframework/abp/tree/dev/modules/cms-kit) (this module is in a very early stage) where you only want to enable the comment feature: |
|||
|
|||
````csharp |
|||
GlobalFeatureManager.Instance.Modules.CmsKit().Comments.Enable(); |
|||
```` |
|||
|
|||
You can check if a feature was enabled: |
|||
|
|||
```csharp |
|||
GlobalFeatureManager.Instance.IsEnabled<CommentsFeature>(); |
|||
``` |
|||
|
|||
Or you can add `[RequiresGlobalFeature(...)]` attribute to a controller/page to disable it if the related feature was disabled: |
|||
|
|||
```csharp |
|||
//... |
|||
[RequiresGlobalFeature(typeof(CommentsFeature))] |
|||
public class CommentController : AbpController |
|||
{ |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
See the issue [#5061](https://github.com/abpframework/abp/issues/5061) until this is fully documented. |
|||
|
|||
### Social/External Logins |
|||
|
|||
Implemented the infrastructure for social/external logins in the account module. So, now you can easily configure your application to support social/external logins by [following the documentation](https://github.com/abpframework/abp/blob/dev/docs/en/Authentication/Social-External-Logins.md). Once you configure a provider, a button will appear on the login page to use this provider. |
|||
|
|||
The social logins will work as expected even if you are using the Angular UI, since the Angular UI uses the MVC login using the authorization code flow implemented with this new version (as explained above). |
|||
|
|||
### Forgot/Reset Password |
|||
|
|||
Implemented forgot password / password reset for the account module. |
|||
|
|||
You can now enter your email address to get an email containing a **password reset link**: |
|||
|
|||
 |
|||
|
|||
When you click to the link, you are redirected to a password reset page to determine your new password: |
|||
|
|||
 |
|||
|
|||
### External Login System |
|||
|
|||
The standard Social/External Login system (like Facebook login) works via OpenID Connect. That means the user is redirected to the login provider, logins there and redirected to your application. |
|||
|
|||
While this is pretty nice for most scenarios, sometimes you want a simpler external login mechanism: User enters username & password in your own application's login form and you check the username & password from another source, not from your own database. |
|||
|
|||
ABP v3.1 introduces an External Login System to check username & password from any source (from an external database, a REST service or from an LDAP / Active Directory server). |
|||
|
|||
You can check the [issue #4977](https://github.com/abpframework/abp/issues/4977#issuecomment-670006297) until it is fully documented. |
|||
|
|||
We've implemented LDAP authentication for the ABP Commercial, using this new login extension system (see the ABP Commercial section below). |
|||
|
|||
### User Security Logs |
|||
|
|||
The new [Security Log System](https://github.com/abpframework/abp/issues/4492) (of the Identity module) automatically logs all authentication related operations (login, logout, change password...) to a `AbpSecurityLogs` table in the database. |
|||
|
|||
### New BLOB Storage Providers |
|||
|
|||
Implemented [AWS](https://github.com/abpframework/abp/blob/dev/docs/en/Blob-Storing-Aws.md) and [Aliyun](https://github.com/abpframework/abp/blob/dev/docs/en/Blob-Storing-Aliyun.md) providers for the [BLOB storing](https://docs.abp.io/en/abp/latest/Blob-Storing) system with this version. |
|||
|
|||
### Other Features / Highlights |
|||
|
|||
Here, some other highlights from this release; |
|||
|
|||
* UOW level caching system [#4796](https://github.com/abpframework/abp/issues/4796) |
|||
* Refactored the console application template to better integrate to the host builder [#5006](https://github.com/abpframework/abp/issues/5006) |
|||
* [Volo.Abp.Ldap](https://www.nuget.org/packages/Volo.Abp.Ldap) package now supports multi-tenancy. |
|||
* Introduce `BasicAggregateRoot` base class [#4808](https://github.com/abpframework/abp/issues/4808) |
|||
* Sets GUID Id in the `InsertAsync` method of the EF Core repository if it was not set by the developer [#4634](https://github.com/abpframework/abp/pull/4634) |
|||
* Added `GetPagedListAsync` methods to the repository to simplify paging [#4617](https://github.com/abpframework/abp/pull/4617) |
|||
* Configured [Prettier](https://prettier.io/) for the startup template [#4318](https://github.com/abpframework/abp/issues/4318) |
|||
* Defined new layout hooks for the MVC UI: before page content & after page content [#4008](https://github.com/abpframework/abp/issues/4008) |
|||
* Allow to put static resources (js, css... files) under the Components folder for ASP.NET Core MVC UI. |
|||
* Upgraded to AutoMapper 10 and Quartz 3.1 for the related integration packages. |
|||
|
|||
## What's New with the ABP Commercial v3.1 RC.1 |
|||
|
|||
### Security Logs UI |
|||
|
|||
We've created a UI to report user security logs for authentication related operations, under the Identity Management menu: |
|||
|
|||
 |
|||
|
|||
Also, every user can see his/her own security logs by selecting the "My security logs" under the user menu: |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
### LDAP Authentication |
|||
|
|||
We've implemented LDAP authentication using the new external login system explained above. Also, created a UI to configure the server settings: |
|||
|
|||
 |
|||
|
|||
In this way, you can simply check passwords of the users from LDAP in the login page. If given username / password doesn't exists on LDAP, then it fallbacks to the local database, just like before. |
|||
|
|||
Since it supports **multi-tenancy**, you can enable, disable and configure it for your tenants. |
|||
|
|||
### Email / Phone Number Verification |
|||
|
|||
User profile management page now supports to Email & Phone Number verification flow: |
|||
|
|||
 |
|||
|
|||
When user clicks to the **verify** button, a verification email/SMS (that has a verification code) sent to the user and the UI waits to submit this code. |
|||
|
|||
### User Lock |
|||
|
|||
Implemented to **lock a user** for a given period of time. Locked users can not login to the application for the given period of time: |
|||
|
|||
 |
|||
|
|||
### ABP Suite: Angular UI Code Generation Revisited |
|||
|
|||
Angular UI code generation has been re-written using the Angular Schematics for the ABP Suite. It is now more stable and produces a better application code. |
|||
|
|||
ABP Suite also supports code generation on module development. |
|||
|
|||
### Others |
|||
|
|||
* **Social logins** and **authorization code flow** are also implemented for the ABP Commercial, just as described above. |
|||
* Added breadcrumb and file icons for the **file management module**. |
|||
|
|||
## The ABP Community |
|||
|
|||
We've lunched the [community.abp.io](https://community.abp.io/) ~two weeks ago with its initial version. It only has "Article submission" system for now. We are developing new exciting features. There will be an update in a few days and we'll publish a new blog post for it. |
|||
|
|||
## Conclusion |
|||
|
|||
The main goals of the 3.1 version were; |
|||
|
|||
* Complete the missing **authentication features** (like social logins, LDAP authentication, authorization code flow for the Angular UI...) for the ABP Framework & ABP Commercial. |
|||
* Re-write a stable and feature complete **Angular service proxy generation** system for the ABP Framework and CRUD UI generation system for the ABP Commercial. |
|||
* Develop a system to lunch **preview versions** of the platform. `3.1.0-rc.1` is the first preview version that has been published with this new system. |
|||
* Complete the fundamental **documentation & tutorials** (we've even created a [video tutorial series](https://www.youtube.com/watch?v=cJzyIFfAlp8&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ)). |
|||
|
|||
ABP.IO platform will be more mature & stable with the v3.1. Enjoy Coding! |
|||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,270 @@ |
|||
## Using DevExtreme with ABP Based Applications |
|||
|
|||
Hi, in this step by step article, I will show you how to integrate DevExtreme components into ABP Framework based applications. |
|||
|
|||
## Install DevExtreme |
|||
|
|||
You can follow [this documentation](https://js.devexpress.com/Documentation/17_1/Guide/ASP.NET_MVC_Controls/Prerequisites_and_Installation/) to install devexpress packages. |
|||
|
|||
## Preparing the Project |
|||
|
|||
### Startup template and the initial run |
|||
|
|||
ABP Framework offers startup templates to get into the business faster. We can download a new startup template using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): |
|||
|
|||
````bash |
|||
abp new DevExtremeSample |
|||
```` |
|||
|
|||
After the download is finished, open the solution in the Visual Studio (or your favorite IDE): |
|||
|
|||
 |
|||
|
|||
Run the `DevExtremeSample.DbMigrator` application to create the database and seed initial data (which creates the admin user, admin role, related permissions, etc). Then we can run the `DevExtremeSample.Web` project to see our application working. |
|||
|
|||
> _Default admin username is **admin** and password is **1q2w3E\***_ |
|||
|
|||
### Adding DevExtreme NuGet Packages |
|||
|
|||
Add the `DevExtreme.AspNet.Core` NuGet package to the `DevExtremeSample.Application.Contracts` project. |
|||
|
|||
``` |
|||
Install-Package DevExtreme.AspNet.Core |
|||
``` |
|||
|
|||
Add the `DevExtreme.AspNet.Data` package to your `DevExtremeSample.Web` project. |
|||
|
|||
``` |
|||
Install-Package DevExtreme.AspNet.Data |
|||
``` |
|||
|
|||
> Please remember that, you must add _"DevExpress NuGet Feed"_ to your **Nuget Package Sources**. Check [this documentation](https://js.devexpress.com/Documentation/17_1/Guide/ASP.NET_MVC_Controls/Prerequisites_and_Installation/) for more information. |
|||
|
|||
### Adding DevExtreme NPM Depencies |
|||
|
|||
Open your `DevExtremeSample.Web` project folder with a command line and add `devextreme` NPM package: |
|||
|
|||
````bash |
|||
npm install devextreme |
|||
```` |
|||
|
|||
### Adding Resource Mappings |
|||
|
|||
The `devextreme` NPM package is saved under `node_modules` folder. We need to move the needed files in our `wwwroot/libs` folder to use them in our web project. We can do it using the ABP [client side resource mapping](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Client-Side-Package-Management) system. |
|||
|
|||
Open the `abp.resourcemapping.js` file in your `DevExtremeSample.Web` project and add the following definition to inside `mappings` object. |
|||
|
|||
````json |
|||
"@node_modules/devextreme/dist/**/*": "@libs/devextreme/" |
|||
```` |
|||
|
|||
The final `abp.resourcemapping.js` file should look like below: |
|||
|
|||
``` |
|||
module.exports = { |
|||
aliases: {}, |
|||
mappings: { |
|||
"@node_modules/devextreme/dist/**/*": "@libs/devextreme/", |
|||
}, |
|||
}; |
|||
``` |
|||
|
|||
Open your `DevExtremeSample.Web` project folder with a command line and run the `gulp` command. This command will copy the needed library files into the ``/wwwroot/libs/devextreme/` folder. |
|||
|
|||
 |
|||
|
|||
You can see `devextreme` folder inside the `wwwroot/libs`: |
|||
|
|||
 |
|||
|
|||
### Adding DevExtremeStyleContributor |
|||
|
|||
We will add DevExtreme CSS files to the global bundle by creating a [bundle contributor](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification). |
|||
|
|||
Create a `Bundling` folder in the `DevExtremeSample.Web` project and a `DevExtremeStyleContributor.cs` file with the following content: |
|||
|
|||
```csharp |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace DevExtremeSample.Web.Bundling |
|||
{ |
|||
public class DevExtremeStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/devextreme/css/dx.common.css"); |
|||
context.Files.AddIfNotContains("/libs/devextreme/css/dx.light.css"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> You can choose another theme than the light theme. Check the `/libs/devextreme/css/` folder and the DevExtreme documentation for other themes. |
|||
|
|||
Open your `DevExtremeSampleWebModule.cs` file in your `DevExtremeSample.Web` project and add following code into the `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.StyleBundles |
|||
.Get(StandardBundles.Styles.Global) |
|||
.AddContributors(typeof(DevExtremeStyleContributor)); |
|||
}); |
|||
``` |
|||
|
|||
### Adding DevExtremeScriptContributor |
|||
|
|||
We cannot add DevExtreme js packages to `Global Script Bundles`, because DevExtreme is using some inline javascript codes and js packages requires to located in the `<head>` section. |
|||
|
|||
But we can create `ViewComponent` and render it at `<head>` section with `AbpLayoutHookOptions`. |
|||
|
|||
First, create `DevExtremeScriptContributor.cs` file at your `Bundling` folder and copy following code block to inside it. |
|||
|
|||
```csharp |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace DevExtremeSample.Web.Bundling |
|||
{ |
|||
[DependsOn( |
|||
typeof(JQueryScriptContributor) |
|||
)] |
|||
public class DevExtremeScriptContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/devextreme/js/dx.all.js"); |
|||
context.Files.AddIfNotContains("/libs/devextreme/js/dx.aspnet.mvc.js"); |
|||
context.Files.AddIfNotContains("/libs/devextreme/js/dx.aspnet.data.js"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see, the `DevExtremeScriptContributor` is depends on `JQueryScriptContributor`. All packages are `JQueryScriptContributor` will added before the `DevExtremeScriptContributor` packages. |
|||
|
|||
#### Create DevExtremeJsViewComponent |
|||
|
|||
Create `Components` folder in your `DevExtremeSample.Web` project. Then create `DevExtremeJs` folder under `Components` folder. |
|||
|
|||
Create `Default.cshtml` file in your `DevExtremeJs` folder and paste following codes to your file. |
|||
|
|||
```csharp |
|||
@using DevExtremeSample.Web.Bundling |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
|
|||
<!-- Devextreme --> |
|||
<abp-script type="typeof(DevExtremeScriptContributor)" /> |
|||
|
|||
``` |
|||
|
|||
Create `DevExtremeJsViewComponent.cs` file in your `DevExtremeJs` folder and paste following codes to your file. |
|||
|
|||
```csharp |
|||
namespace DevExtremeSample.Web.Components.DevExtremeJs |
|||
{ |
|||
public class DevExtremeJsViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View("/Components/DevExtremeJs/Default.cshtml"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
After that, your `*.Web` project should be like as following. |
|||
|
|||
 |
|||
|
|||
Then we can add this view component to `<head>` section by using **hooks**. |
|||
|
|||
Open your `DevExtremeSampleWebModule.cs` file in your `DevExtremeSample.Web` project and add following code to `ConfigureServices` method. |
|||
|
|||
```csharp |
|||
Configure<AbpLayoutHookOptions>(options => |
|||
{ |
|||
options.Add( |
|||
LayoutHooks.Head.Last, //The hook name |
|||
typeof(DevExtremeJsViewComponent) //The component to add |
|||
); |
|||
}); |
|||
``` |
|||
|
|||
#### Known Issue: Uncaught TypeError: MutationObserver.observe: Argument 1 is not an object. |
|||
|
|||
> If you are using ABP 3.1 version or more, you can skip this section. |
|||
|
|||
When you run your `*.Web` project, you will see an exception (`Uncaught TypeError: MutationObserver.observe: Argument 1 is not an object.`) at your console. |
|||
|
|||
This article is written in ABP Version 3.1 development cycle. We have fixed this issue [(ref)](https://github.com/abpframework/abp/issues/4566) in this development cycle. |
|||
|
|||
To fix that issue, download this file [abp.jquery.js](https://github.com/abpframework/abp/blob/dev/npm/packs/jquery/src/abp.jquery.js) and replace with `wwwroot / libs / abp / jquery / abp.jquery.js` file of your `*.Web` project. |
|||
|
|||
### Result |
|||
|
|||
After following this step-by-step article you can use all DevExtreme features in your project. |
|||
|
|||
 |
|||
|
|||
> The result example is created by following [this documentation](https://js.devexpress.com/Demos/WidgetsGallery/Demo/ProgressBar/Overview/NetCore/Light/). |
|||
|
|||
--- |
|||
|
|||
### Sample Application |
|||
|
|||
We have created a sample application with [Tree List](https://demos.devexpress.com/ASPNetCore/Demo/TreeList/Overview/) and [Data Grid](https://demos.devexpress.com/ASPNetCore/Demo/DataGrid/Overview/) examples. |
|||
|
|||
You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Mvc). |
|||
|
|||
We have some notes about this sample and general usages of DevExtreme at ABP based application. |
|||
|
|||
### Data Storage |
|||
|
|||
We will use an in-memory list for using data storage for this sample. |
|||
|
|||
There is a `SampleDataService.cs` file in `Data` folder at `*.Application.Contracts` project. We store all sample data here. |
|||
|
|||
We did not create `Entities` etc. Because we want to show "How to use DevExtreme?", because of that, in this sample we focused to application and UI layer. |
|||
|
|||
### JSON Serialization |
|||
|
|||
You can see some `[JsonProperty(Name = "OrderId")]` attributes at DTO's. In this sample, we use that attribute on DTO's properties because DevExtreme official resource is suggesting to _disable the conversion in the JSON serializer_ [(ref)](https://js.devexpress.com/Documentation/19_1/Guide/Angular_Components/Visual_Studio_Integration/Add_DevExtreme_to_an_ASP.NET_Core_Angular_Application/#Troubleshooting). **DO NOT DO THAT!** |
|||
|
|||
If you change **the conversion in the JSON serializer**, some pre-build abp modules may occur a problem. |
|||
|
|||
### MVC |
|||
|
|||
You can use some DevExtreme functions to create UI. The following code blocks show you how you can use it with ABP Applicaion Services. |
|||
|
|||
```csharp |
|||
Html.DevExtreme().DataGrid<Order>() |
|||
.DataSource(d => d.Mvc() |
|||
.Controller("Order") // Application Service Name 'without **AppService**' |
|||
.LoadAction("GetOrders") // Method Name 'without **Async**' |
|||
.InsertAction("InsertOrder") |
|||
.UpdateAction("UpdateOrder") |
|||
.DeleteAction("DeleteOrder") |
|||
.Key("OrderID") |
|||
) |
|||
``` |
|||
|
|||
```csharp |
|||
public class OrderAppService : DevExtremeSampleAppService, IOrderAppService |
|||
{ |
|||
public async Task<LoadResult> GetOrdersAsync(DataSourceLoadOptions loadOptions) |
|||
{ |
|||
... |
|||
} |
|||
|
|||
public async Task<Order> InsertOrder(string values) |
|||
{ |
|||
... |
|||
} |
|||
... |
|||
} |
|||
``` |
|||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@ -1,3 +1,250 @@ |
|||
# Emailing |
|||
# Email Sending |
|||
|
|||
TODO! |
|||
ABP Framework provides various services, settings and integrations for sending emails; |
|||
|
|||
* Provides `IEmailSender` service that is used to send emails. |
|||
* Defines [settings](Settings.md) to configure email sending. |
|||
* Integrates to the [background job system](Background-Jobs.md) to send emails via background jobs. |
|||
* Provides [MailKit integration](MailKit.md) package. |
|||
|
|||
## Installation |
|||
|
|||
> This package is already installed if you are using the [application startup template](Startup-Templates/Application.md). |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.Emailing |
|||
```` |
|||
|
|||
If you haven't done it yet, you first need to install the ABP CLI. For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.Emailing). |
|||
|
|||
## Sending Emails |
|||
|
|||
### IEmailSender |
|||
|
|||
[Inject](Dependency-Injection.md) the `IEmailSender` into any service and use the `SendAsync` method to send emails. |
|||
|
|||
**Example** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Emailing; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IEmailSender _emailSender; |
|||
|
|||
public MyService(IEmailSender emailSender) |
|||
{ |
|||
_emailSender = emailSender; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
await _emailSender.SendAsync( |
|||
"target@domain.com", // target email address |
|||
"Email subject", // subject |
|||
"This is email body..." // email body |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`SendAsync` method has overloads to supply more parameters like; |
|||
|
|||
* **from**: You can set this as the first argument to set a sender email address. If not provided, the default sender address is used (see the email settings below). |
|||
* **isBodyHtml**: Indicates whether the email body may contain HTML tags. **Default: true**. |
|||
|
|||
> `IEmailSender` is the suggested way to send emails, since it makes your code provider independent. |
|||
|
|||
#### MailMessage |
|||
|
|||
In addition to primitive parameters, you can pass a **standard `MailMessage` object** ([see](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage)) to the `SendAsync` method to set more options, like adding attachments. |
|||
|
|||
### ISmtpEmailSender |
|||
|
|||
Sending emails is implemented by the standard `SmtpClient` class ([see](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient)) by default. The implementation class is the `SmtpEmailSender`. This class also expose the `ISmtpEmailSender` service (in addition to the `IEmailSender`). |
|||
|
|||
Most of the time you want to directly use the `IEmailSender` to make your code provider independent. However, if you want to create an `SmtpClient` object with the same email settings, you can inject the `ISmtpEmailSender` and use its `BuildClientAsync` method to obtain a `SmtpClient` object and send the email yourself. |
|||
|
|||
## Queueing Emails / Background Jobs |
|||
|
|||
`IEmailSender` has a `QueueAsync` method that can be used to add emails to the background job queue to send them in a background thread. In this way, you don't take time of the user by waiting to send the email. `QueueAsync` method gets the same arguments with the `SendAsync` method. |
|||
|
|||
Queueing emails tolerates errors since the background job system has re-try mechanism to overcome temporary network/server problems. |
|||
|
|||
See the [background jobs document](Background-Jobs.md) for more about the background job system. |
|||
|
|||
## Email Settings |
|||
|
|||
Email sending uses the [setting system](Settings.md) to define settings and get the values of these settings on the runtime. `Volo.Abp.Emailing.EmailSettingNames` defines constants for the setting names, just listed below: |
|||
|
|||
* **Abp.Mailing.DefaultFromAddress**: Used as the sender's email address when you don't specify a sender when sending emails (just like in the example above). |
|||
* **Abp.Mailing.DefaultFromDisplayName**: Used as the sender's display name when you don't specify a sender when sending emails (just like in the example above). |
|||
* **Abp.Mailing.Smtp.Host**: The IP/Domain of the SMTP server (default: 127.0.0.1). |
|||
* **Abp.Mailing.Smtp.Port**: The Port of the SMTP server (default: 25). |
|||
* **Abp.Mailing.Smtp.UserName**: Username, if the SMTP server requires authentication. |
|||
* **Abp.Mailing.Smtp.Password**: Password, if the SMTP server requires authentication. **This value is encrypted **(see the section below). |
|||
* **Abp.Mailing.Smtp.Domain**: Domain for the username, if the SMTP server requires authentication. |
|||
* **Abp.Mailing.Smtp.EnableSsl**: A value that indicates if the SMTP server uses SSL or not ("true" or "false". Default: "false"). |
|||
* **Abp.Mailing.Smtp.UseDefaultCredentials**: If true, uses default credentials instead of the provided username and password ("true" or "false". Default: "true"). |
|||
|
|||
The easiest way to define these settings it to add them to the `appsettings.json` file. The [application startup template](Startup-Templates/Application.md) already has these settings in the `appsettings.json`: |
|||
|
|||
````json |
|||
"Settings": { |
|||
"Abp.Mailing.Smtp.Host": "127.0.0.1", |
|||
"Abp.Mailing.Smtp.Port": "25", |
|||
"Abp.Mailing.Smtp.UserName": "", |
|||
"Abp.Mailing.Smtp.Password": "", |
|||
"Abp.Mailing.Smtp.Domain": "", |
|||
"Abp.Mailing.Smtp.EnableSsl": "false", |
|||
"Abp.Mailing.Smtp.UseDefaultCredentials": "true", |
|||
"Abp.Mailing.DefaultFromAddress": "noreply@abp.io", |
|||
"Abp.Mailing.DefaultFromDisplayName": "ABP application" |
|||
} |
|||
```` |
|||
|
|||
You can set/change these settings using the `ISettingManager` and store values in a database. See the [setting system document](Settings.md) to understand the setting system better. |
|||
|
|||
### Encrypt the SMTP Password |
|||
|
|||
*Abp.Mailing.Smtp.Password* must be an **encrypted** value. If you use the `ISettingManager` to set the password, you don't have to worry. It internally encrypts the values on set and decrypts on get. |
|||
|
|||
If you use the `appsettings.json` to store the password, you should manually inject the `ISettingEncryptionService` and use its `Encrypt` method to obtain an encrypted value. This can be done by creating a simple code in your application. Then you can delete the code. As better, you can create a UI in your application to configure the email settings. In this case, you can directly use the `ISettingManager` without worrying the encryption. |
|||
|
|||
### ISmtpEmailSenderConfiguration |
|||
|
|||
If you don't want to use the setting system to store the email sending configuration, you can replace the `ISmtpEmailSenderConfiguration` service with your own implementation to get the configuration from any other source. `ISmtpEmailSenderConfiguration` is implemented by the `SmtpEmailSenderConfiguration` by default, which gets the configuration from the setting system as explained above. |
|||
|
|||
## Text Template Integration |
|||
|
|||
ABP Framework provides a strong and flexible [text templating system](Text-Templating.md). You can use the text templating system to create dynamic email contents. Inject the `ITemplateRenderer` and use the `RenderAsync` to render a template. Then use the result as the email body. |
|||
|
|||
While you can define and use your own text templates, email sending system provides two simple built-in text templates. |
|||
|
|||
**Example: Use the standard and simple message template to send emails** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Emailing; |
|||
using Volo.Abp.Emailing.Templates; |
|||
using Volo.Abp.TextTemplating; |
|||
|
|||
namespace Acme.BookStore.Web |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IEmailSender _emailSender; |
|||
private readonly ITemplateRenderer _templateRenderer; |
|||
|
|||
public MyService( |
|||
IEmailSender emailSender, |
|||
ITemplateRenderer templateRenderer) |
|||
{ |
|||
_emailSender = emailSender; |
|||
_templateRenderer = templateRenderer; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
var body = await _templateRenderer.RenderAsync( |
|||
StandardEmailTemplates.Message, |
|||
new |
|||
{ |
|||
message = "This is email body..." |
|||
} |
|||
); |
|||
|
|||
await _emailSender.SendAsync( |
|||
"target-address@domain.com", |
|||
"Email subject", |
|||
body |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
The resulting email body will be shown below: |
|||
|
|||
````html |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
This is email body... |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
Emailing system defines the built-in text templates with the given names: |
|||
|
|||
"**Abp.StandardEmailTemplates.Message**" is simplest template that has a text message: |
|||
|
|||
````html |
|||
{%{{{model.message}}}%} |
|||
```` |
|||
|
|||
This template uses the "Abp.StandardEmailTemplates.Layout" as its layout. |
|||
|
|||
"**Abp.StandardEmailTemplates.Layout**" is a simple template to provide an HTML document layout: |
|||
|
|||
````html |
|||
<!DOCTYPE html> |
|||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
</head> |
|||
<body> |
|||
{%{{{content}}}%} |
|||
</body> |
|||
</html> |
|||
```` |
|||
|
|||
The final rendered message was shown above. |
|||
|
|||
> These template names are contants defined in the `Volo.Abp.Emailing.Templates.StandardEmailTemplates` class. |
|||
|
|||
### Overriding/Replacing the Standard Templates |
|||
|
|||
You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](Virtual-File-System.md) (VFS) or replace them in your own template definition provider. |
|||
|
|||
Pathes of the templates in the virtual file system are shown below: |
|||
|
|||
* `/Volo/Abp/Emailing/Templates/Layout.tpl` |
|||
* `/Volo/Abp/Emailing/Templates/Message.tpl` |
|||
|
|||
If you add files to the same localization in the virtual file system, your files will override them. |
|||
|
|||
Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural. |
|||
|
|||
See the [text templating system](Text-Templating.md) document for details. |
|||
|
|||
> Notice that you can define and use your own templates for your application, rather than using the standard simple templates. These standard templates are mostly for reusable modules where they don't define their own templates but rely on the built-in ones. This makes easy to customize emails sent by the used modules, by just overriding the standard email layout template. |
|||
|
|||
## NullEmailSender |
|||
|
|||
`NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the [standard log system](Logging.md), rathen than actually sending the emails. |
|||
|
|||
This class can be useful especially in development time where you generally don't want to send real emails. The [application startup template](Startup-Templates/Application.md) already uses this class in the **DEBUG mode** with the following configuration in the domain layer: |
|||
|
|||
````csharp |
|||
#if DEBUG |
|||
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>()); |
|||
#endif |
|||
```` |
|||
|
|||
So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent as expected on production (RELEASE mode). Remove these lines if you want to send real emails on DEBUG too. |
|||
|
|||
## See Also |
|||
|
|||
* [MailKit integration for sending emails](MailKit.md) |
|||
@ -0,0 +1,3 @@ |
|||
# Global Features |
|||
|
|||
TODO (see [#5061](https://github.com/abpframework/abp/issues/5061) until this is documented). |
|||
@ -0,0 +1,48 @@ |
|||
# MailKit Integration |
|||
|
|||
[MailKit](http://www.mimekit.net/) is a cross-platform, popular open source mail client library for .net. ABP Framework provides an integration package to use the MailKit as the [email sender](Emailing.md). |
|||
|
|||
## Installation |
|||
|
|||
It is suggested to use the [ABP CLI](CLI.md) to install this package. Open a command line window in the folder of the project (.csproj file) and type the following command: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.MailKit |
|||
```` |
|||
|
|||
If you haven't done it yet, you first need to install the ABP CLI. For other installation options, see [the package description page](https://abp.io/package-detail/Volo.Abp.MailKit). |
|||
|
|||
## Sending Emails |
|||
|
|||
### IEmailSender |
|||
|
|||
[Inject](Dependency-Injection.md) the standard `IEmailSender` into any service and use the `SendAsync` method to send emails. See the [email sending document](Emailing.md) for details. |
|||
|
|||
> `IEmailSender` is the suggested way to send emails even if you use MailKit, since it makes your code provider independent. |
|||
|
|||
### IMailKitSmtpEmailSender |
|||
|
|||
MailKit package also exposes the `IMailKitSmtpEmailSender` service that extends the `IEmailSender` by adding the `BuildClientAsync()` method. This method can be used to obtain a `MailKit.Net.Smtp.SmtpClient` object that can be used to perform MailKit specific operations. |
|||
|
|||
## Configuration |
|||
|
|||
MailKit integration package uses the same settings defined by the email sending system. So, refer to the [email sending document](Emailing.md) for the settings. |
|||
|
|||
In addition to the standard settings, this package defines `AbpMailKitOptions` as a simple [options](Options.md) class. This class defines only one options: |
|||
|
|||
* **SecureSocketOption**: Used to set one of the `SecureSocketOptions`. Default: `null` (uses the defaults). |
|||
|
|||
**Example: Use *SecureSocketOptions.SslOnConnect*** |
|||
|
|||
````csharp |
|||
Configure<AbpMailKitOptions>(options => |
|||
{ |
|||
options.SecureSocketOption = SecureSocketOptions.SslOnConnect; |
|||
}); |
|||
```` |
|||
|
|||
Refer to the [MailKit documentation](http://www.mimekit.net/) to learn more about this option. |
|||
|
|||
## See Also |
|||
|
|||
* [Email sending](Emailing.md) |
|||
@ -0,0 +1,3 @@ |
|||
# Module Entity Extensions |
|||
|
|||
See https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions (it will be moved here soon). |
|||
@ -0,0 +1,40 @@ |
|||
# Preview Releases |
|||
|
|||
The preview versions are released **~2 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions. |
|||
|
|||
Versioning of a preview release is like that: |
|||
|
|||
* 3.1.0-rc.1 |
|||
* 4.0.0-rc.1 |
|||
|
|||
More than one preview releases (like 3.1.0-rc.2 and 3.1.0-rc.3) might be published until the stable version (like 3.1.0). |
|||
|
|||
## Using the Preview Versions |
|||
|
|||
### New Solutions |
|||
|
|||
To create a project for testing the preview version, you can select the "**preview**" option on the [download page](https://abp.io/get-started) or use the "**--preview**" parameter with the [ABP CLI](CLI.md) new command: |
|||
|
|||
````bash |
|||
abp new Acme.BookStore --preview |
|||
```` |
|||
|
|||
This command will create a new project using the latest preview NuGet packages, NPM packages and the solution template. Whenever the stable version is released, you can switch to the stable version for your solution using the `abp switch-to-stable` command in the root folder of your solution. |
|||
|
|||
### Existing Solutions |
|||
|
|||
If you already have a solution and want to use/test the latest preview version, use the following [ABP CLI](CLI.md) command in the root folder of your solution. |
|||
|
|||
````bash |
|||
abp switch-to-preview |
|||
```` |
|||
|
|||
You can return back to the latest stable using the `abp switch-to-stable ` command later. |
|||
|
|||
````bash |
|||
abp switch-to-stable |
|||
```` |
|||
|
|||
## Providing Feedback |
|||
|
|||
You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback. |
|||
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,15 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.UI |
|||
{ |
|||
public class AbpMvcUiOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Default value: "/Account/Login".
|
|||
/// </summary>
|
|||
public string LoginUrl { get; set; } = "/Account/Login"; |
|||
|
|||
/// <summary>
|
|||
/// Default value: "/Account/Logout".
|
|||
/// </summary>
|
|||
public string LogoutUrl { get; set; } = "/Account/Logout"; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.Abstractions; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.GlobalFeatures; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures |
|||
{ |
|||
public class GlobalFeatureActionFilter : IAsyncActionFilter, ITransientDependency |
|||
{ |
|||
public ILogger<GlobalFeatureActionFilter> Logger { get; set; } |
|||
|
|||
public GlobalFeatureActionFilter() |
|||
{ |
|||
Logger = NullLogger<GlobalFeatureActionFilter>.Instance; |
|||
} |
|||
|
|||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) |
|||
{ |
|||
if (!context.ActionDescriptor.IsControllerAction()) |
|||
{ |
|||
await next(); |
|||
return; |
|||
} |
|||
|
|||
if (!IsGlobalFeatureEnabled(context.Controller.GetType(), out var attribute)) |
|||
{ |
|||
Logger.LogWarning($"The '{context.Controller.GetType().FullName}' controller needs to enable '{attribute.Name}' feature."); |
|||
context.Result = new NotFoundResult(); |
|||
return; |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected virtual bool IsGlobalFeatureEnabled(Type controllerType, out RequiresGlobalFeatureAttribute attribute) |
|||
{ |
|||
attribute = ReflectionHelper.GetSingleAttributeOrDefault<RequiresGlobalFeatureAttribute>(controllerType); |
|||
return attribute == null || GlobalFeatureManager.Instance.IsEnabled(attribute.GetFeatureName()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.AspNetCore.Mvc.Abstractions; |
|||
using Microsoft.AspNetCore.Mvc.Filters; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.GlobalFeatures; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures |
|||
{ |
|||
public class GlobalFeaturePageFilter: IAsyncPageFilter, ITransientDependency |
|||
{ |
|||
public ILogger<GlobalFeaturePageFilter> Logger { get; set; } |
|||
|
|||
public GlobalFeaturePageFilter() |
|||
{ |
|||
Logger = NullLogger<GlobalFeaturePageFilter>.Instance; |
|||
} |
|||
|
|||
public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) |
|||
{ |
|||
if (context.HandlerInstance == null || !context.ActionDescriptor.IsPageAction()) |
|||
{ |
|||
await next(); |
|||
return; |
|||
} |
|||
|
|||
if (!IsGlobalFeatureEnabled(context.HandlerInstance.GetType(), out var attribute)) |
|||
{ |
|||
Logger.LogWarning($"The '{context.HandlerInstance.GetType().FullName}' page needs to enable '{attribute.Name}' feature."); |
|||
context.Result = new NotFoundResult(); |
|||
return; |
|||
} |
|||
|
|||
await next(); |
|||
} |
|||
|
|||
protected virtual bool IsGlobalFeatureEnabled(Type controllerType, out RequiresGlobalFeatureAttribute attribute) |
|||
{ |
|||
attribute = ReflectionHelper.GetSingleAttributeOrDefault<RequiresGlobalFeatureAttribute>(controllerType); |
|||
return attribute == null || GlobalFeatureManager.Instance.IsEnabled(attribute.GetFeatureName()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Volo.Abp.AspNetCore.ExceptionHandling |
|||
{ |
|||
public class AbpExceptionHandlingOptions |
|||
{ |
|||
public bool SendExceptionsDetailsToClients { get; set; } = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,269 @@ |
|||
using System.Collections.ObjectModel; |
|||
using System.Linq.Expressions; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace System.Linq |
|||
{ |
|||
// Codes below are taken from https://github.com/scottksmith95/LINQKit project.
|
|||
|
|||
/// <summary> The Predicate Operator </summary>
|
|||
public enum PredicateOperator |
|||
{ |
|||
/// <summary> The "Or" </summary>
|
|||
Or, |
|||
|
|||
/// <summary> The "And" </summary>
|
|||
And |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// See http://www.albahari.com/expressions for information and examples.
|
|||
/// </summary>
|
|||
public static class PredicateBuilder |
|||
{ |
|||
private class RebindParameterVisitor : ExpressionVisitor |
|||
{ |
|||
private readonly ParameterExpression _oldParameter; |
|||
private readonly ParameterExpression _newParameter; |
|||
|
|||
public RebindParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter) |
|||
{ |
|||
_oldParameter = oldParameter; |
|||
_newParameter = newParameter; |
|||
} |
|||
|
|||
protected override Expression VisitParameter(ParameterExpression node) |
|||
{ |
|||
return node == _oldParameter ? _newParameter : base.VisitParameter(node); |
|||
} |
|||
} |
|||
|
|||
/// <summary> Start an expression </summary>
|
|||
public static ExpressionStarter<T> New<T>(Expression<Func<T, bool>> expr = null) |
|||
{ |
|||
return new ExpressionStarter<T>(expr); |
|||
} |
|||
|
|||
/// <summary> Create an expression with a stub expression true or false to use when the expression is not yet started. </summary>
|
|||
public static ExpressionStarter<T> New<T>(bool defaultExpression) |
|||
{ |
|||
return new ExpressionStarter<T>(defaultExpression); |
|||
} |
|||
|
|||
/// <summary> OR </summary>
|
|||
public static Expression<Func<T, bool>> Or<T>([NotNull] this Expression<Func<T, bool>> expr1, |
|||
[NotNull] Expression<Func<T, bool>> expr2) |
|||
{ |
|||
var expr2Body = new RebindParameterVisitor(expr2.Parameters[0], expr1.Parameters[0]).Visit(expr2.Body); |
|||
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, expr2Body), expr1.Parameters); |
|||
} |
|||
|
|||
/// <summary> AND </summary>
|
|||
public static Expression<Func<T, bool>> And<T>([NotNull] this Expression<Func<T, bool>> expr1, |
|||
[NotNull] Expression<Func<T, bool>> expr2) |
|||
{ |
|||
var expr2Body = new RebindParameterVisitor(expr2.Parameters[0], expr1.Parameters[0]).Visit(expr2.Body); |
|||
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, expr2Body), expr1.Parameters); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Extends the specified source Predicate with another Predicate and the specified PredicateOperator.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type</typeparam>
|
|||
/// <param name="first">The source Predicate.</param>
|
|||
/// <param name="second">The second Predicate.</param>
|
|||
/// <param name="operator">The Operator (can be "And" or "Or").</param>
|
|||
/// <returns>Expression{Func{T, bool}}</returns>
|
|||
public static Expression<Func<T, bool>> Extend<T>([NotNull] this Expression<Func<T, bool>> first, |
|||
[NotNull] Expression<Func<T, bool>> second, PredicateOperator @operator = PredicateOperator.Or) |
|||
{ |
|||
return @operator == PredicateOperator.Or ? first.Or(second) : first.And(second); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Extends the specified source Predicate with another Predicate and the specified PredicateOperator.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type</typeparam>
|
|||
/// <param name="first">The source Predicate.</param>
|
|||
/// <param name="second">The second Predicate.</param>
|
|||
/// <param name="operator">The Operator (can be "And" or "Or").</param>
|
|||
/// <returns>Expression{Func{T, bool}}</returns>
|
|||
public static Expression<Func<T, bool>> Extend<T>([NotNull] this ExpressionStarter<T> first, |
|||
[NotNull] Expression<Func<T, bool>> second, PredicateOperator @operator = PredicateOperator.Or) |
|||
{ |
|||
return @operator == PredicateOperator.Or ? first.Or(second) : first.And(second); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// ExpressionStarter{T} which eliminates the default 1=0 or 1=1 stub expressions
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type</typeparam>
|
|||
public class ExpressionStarter<T> |
|||
{ |
|||
public ExpressionStarter() : this(false) |
|||
{ |
|||
} |
|||
|
|||
public ExpressionStarter(bool defaultExpression) |
|||
{ |
|||
if (defaultExpression) |
|||
{ |
|||
DefaultExpression = f => true; |
|||
} |
|||
else |
|||
{ |
|||
DefaultExpression = f => false; |
|||
} |
|||
} |
|||
|
|||
public ExpressionStarter(Expression<Func<T, bool>> exp) : this(false) |
|||
{ |
|||
_predicate = exp; |
|||
} |
|||
|
|||
/// <summary>The actual Predicate. It can only be set by calling Start.</summary>
|
|||
private Expression<Func<T, bool>> Predicate => |
|||
(IsStarted || !UseDefaultExpression) ? _predicate : DefaultExpression; |
|||
|
|||
private Expression<Func<T, bool>> _predicate; |
|||
|
|||
/// <summary>Determines if the predicate is started.</summary>
|
|||
public bool IsStarted => _predicate != null; |
|||
|
|||
/// <summary> A default expression to use only when the expression is null </summary>
|
|||
public bool UseDefaultExpression => DefaultExpression != null; |
|||
|
|||
/// <summary>The default expression</summary>
|
|||
public Expression<Func<T, bool>> DefaultExpression { get; set; } |
|||
|
|||
/// <summary>Set the Expression predicate</summary>
|
|||
/// <param name="exp">The first expression</param>
|
|||
public Expression<Func<T, bool>> Start(Expression<Func<T, bool>> exp) |
|||
{ |
|||
if (IsStarted) |
|||
{ |
|||
throw new Exception("Predicate cannot be started again."); |
|||
} |
|||
|
|||
return _predicate = exp; |
|||
} |
|||
|
|||
/// <summary>Or</summary>
|
|||
public Expression<Func<T, bool>> Or([NotNull] Expression<Func<T, bool>> expr2) |
|||
{ |
|||
return (IsStarted) ? _predicate = Predicate.Or(expr2) : Start(expr2); |
|||
} |
|||
|
|||
/// <summary>And</summary>
|
|||
public Expression<Func<T, bool>> And([NotNull] Expression<Func<T, bool>> expr2) |
|||
{ |
|||
return (IsStarted) ? _predicate = Predicate.And(expr2) : Start(expr2); |
|||
} |
|||
|
|||
/// <summary> Show predicate string </summary>
|
|||
public override string ToString() |
|||
{ |
|||
return Predicate?.ToString(); |
|||
} |
|||
|
|||
#region Implicit Operators
|
|||
|
|||
/// <summary>
|
|||
/// Allows this object to be implicitely converted to an Expression{Func{T, bool}}.
|
|||
/// </summary>
|
|||
/// <param name="right"></param>
|
|||
public static implicit operator Expression<Func<T, bool>>(ExpressionStarter<T> right) |
|||
{ |
|||
return right?.Predicate; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows this object to be implicitely converted to an Expression{Func{T, bool}}.
|
|||
/// </summary>
|
|||
/// <param name="right"></param>
|
|||
public static implicit operator Func<T, bool>(ExpressionStarter<T> right) |
|||
{ |
|||
return right == null ? null : |
|||
(right.IsStarted || right.UseDefaultExpression) ? right.Predicate.Compile() : null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows this object to be implicitely converted to an Expression{Func{T, bool}}.
|
|||
/// </summary>
|
|||
/// <param name="right"></param>
|
|||
public static implicit operator ExpressionStarter<T>(Expression<Func<T, bool>> right) |
|||
{ |
|||
return right == null ? null : new ExpressionStarter<T>(right); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
#region Implement Expression<TDelagate> methods and properties
|
|||
|
|||
#if !(NET35)
|
|||
|
|||
/// <summary></summary>
|
|||
public Func<T, bool> Compile() |
|||
{ |
|||
return Predicate.Compile(); |
|||
} |
|||
#endif
|
|||
|
|||
#if !(NET35 || WINDOWS_APP || NETSTANDARD || PORTABLE || PORTABLE40 || UAP)
|
|||
/// <summary></summary>
|
|||
public Func<T, bool> Compile(DebugInfoGenerator debugInfoGenerator) { return Predicate.Compile(debugInfoGenerator); } |
|||
|
|||
/// <summary></summary>
|
|||
public Expression<Func<T, bool>> Update(Expression body, IEnumerable<ParameterExpression> parameters) { return Predicate.Update(body, parameters); } |
|||
#endif
|
|||
|
|||
#endregion
|
|||
|
|||
#region Implement LamdaExpression methods and properties
|
|||
|
|||
/// <summary></summary>
|
|||
public Expression Body => Predicate.Body; |
|||
|
|||
|
|||
/// <summary></summary>
|
|||
public ExpressionType NodeType => Predicate.NodeType; |
|||
|
|||
/// <summary></summary>
|
|||
public ReadOnlyCollection<ParameterExpression> Parameters => Predicate.Parameters; |
|||
|
|||
/// <summary></summary>
|
|||
public Type Type => Predicate.Type; |
|||
|
|||
#if !(NET35)
|
|||
/// <summary></summary>
|
|||
public string Name => Predicate.Name; |
|||
|
|||
/// <summary></summary>
|
|||
public Type ReturnType => Predicate.ReturnType; |
|||
|
|||
/// <summary></summary>
|
|||
public bool TailCall => Predicate.TailCall; |
|||
#endif
|
|||
|
|||
#if !(NET35 || WINDOWS_APP || NETSTANDARD || PORTABLE || PORTABLE40 || UAP)
|
|||
/// <summary></summary>
|
|||
public void CompileToMethod(MethodBuilder method) { Predicate.CompileToMethod(method); } |
|||
|
|||
/// <summary></summary>
|
|||
public void CompileToMethod(MethodBuilder method, DebugInfoGenerator debugInfoGenerator) { Predicate.CompileToMethod(method, debugInfoGenerator); } |
|||
|
|||
#endif
|
|||
|
|||
#endregion
|
|||
|
|||
#region Implement Expression methods and properties
|
|||
|
|||
#if !(NET35)
|
|||
/// <summary></summary>
|
|||
public virtual bool CanReduce => Predicate.CanReduce; |
|||
#endif
|
|||
|
|||
#endregion
|
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
[assembly:InternalsVisibleTo("Volo.Abp.GlobalFeatures.Tests")] |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<AssemblyName>Volo.Abp.GlobalFeatures</AssemblyName> |
|||
<PackageId>Volo.Abp.GlobalFeatures</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public class AbpGlobalFeaturesModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public abstract class GlobalFeature |
|||
{ |
|||
[NotNull] |
|||
public GlobalModuleFeatures Module { get; } |
|||
|
|||
[NotNull] |
|||
public GlobalFeatureManager FeatureManager { get; } |
|||
|
|||
[NotNull] |
|||
public string FeatureName { get; } |
|||
|
|||
public bool IsEnabled |
|||
{ |
|||
get => FeatureManager.IsEnabled(FeatureName); |
|||
set => SetEnabled(value); |
|||
} |
|||
|
|||
protected GlobalFeature([NotNull] GlobalModuleFeatures module) |
|||
{ |
|||
Module = Check.NotNull(module, nameof(module)); |
|||
FeatureManager = Module.FeatureManager; |
|||
FeatureName = GlobalFeatureNameAttribute.GetName(GetType()); |
|||
} |
|||
|
|||
public virtual void Enable() |
|||
{ |
|||
FeatureManager.Enable(FeatureName); |
|||
} |
|||
|
|||
public virtual void Disable() |
|||
{ |
|||
FeatureManager.Disable(FeatureName); |
|||
} |
|||
|
|||
public void SetEnabled(bool isEnabled) |
|||
{ |
|||
if (isEnabled) |
|||
{ |
|||
Enable(); |
|||
} |
|||
else |
|||
{ |
|||
Disable(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public class GlobalFeatureDictionary : Dictionary<string, GlobalFeature> |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public class GlobalFeatureManager |
|||
{ |
|||
public static GlobalFeatureManager Instance { get; protected set; } = new GlobalFeatureManager(); |
|||
|
|||
/// <summary>
|
|||
/// A common dictionary to store arbitrary configurations.
|
|||
/// </summary>
|
|||
[NotNull] |
|||
public Dictionary<object, object> Configuration { get; } |
|||
|
|||
public GlobalModuleFeaturesDictionary Modules { get; } |
|||
|
|||
protected HashSet<string> EnabledFeatures { get; } |
|||
|
|||
internal GlobalFeatureManager() |
|||
{ |
|||
EnabledFeatures = new HashSet<string>(); |
|||
Configuration = new Dictionary<object, object>(); |
|||
Modules = new GlobalModuleFeaturesDictionary(this); |
|||
} |
|||
|
|||
public virtual bool IsEnabled<TFeature>() |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
return IsEnabled(GlobalFeatureNameAttribute.GetName<TFeature>()); |
|||
} |
|||
|
|||
public virtual bool IsEnabled([NotNull] Type featureType) |
|||
{ |
|||
return IsEnabled(GlobalFeatureNameAttribute.GetName(featureType)); |
|||
} |
|||
|
|||
public virtual bool IsEnabled(string featureName) |
|||
{ |
|||
return EnabledFeatures.Contains(featureName); |
|||
} |
|||
|
|||
protected internal void Enable(string featureName) |
|||
{ |
|||
EnabledFeatures.AddIfNotContains(featureName); |
|||
} |
|||
|
|||
protected internal void Disable(string featureName) |
|||
{ |
|||
EnabledFeatures.Remove(featureName); |
|||
} |
|||
|
|||
public virtual IEnumerable<string> GetEnabledFeatureNames() |
|||
{ |
|||
return EnabledFeatures; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
[AttributeUsage(AttributeTargets.Class)] |
|||
public class GlobalFeatureNameAttribute : Attribute |
|||
{ |
|||
[NotNull] |
|||
public string Name { get; } |
|||
|
|||
public GlobalFeatureNameAttribute([NotNull] string name) |
|||
{ |
|||
Name = Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
} |
|||
|
|||
public static string GetName<TFeature>() |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
return GetName(typeof(TFeature)); |
|||
} |
|||
|
|||
[NotNull] |
|||
public static string GetName([NotNull] Type type) |
|||
{ |
|||
Check.NotNull(type, nameof(type)); |
|||
|
|||
var attribute = type |
|||
.GetCustomAttributes<GlobalFeatureNameAttribute>() |
|||
.FirstOrDefault(); |
|||
|
|||
if (attribute == null) |
|||
{ |
|||
throw new AbpException($"{type.AssemblyQualifiedName} should define the {typeof(GlobalFeatureNameAttribute).FullName} atttribute!"); |
|||
} |
|||
|
|||
return attribute |
|||
.As<GlobalFeatureNameAttribute>() |
|||
.Name; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
using System.Collections.Generic; |
|||
using System.Collections.Immutable; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public abstract class GlobalModuleFeatures |
|||
{ |
|||
[NotNull] |
|||
public GlobalFeatureManager FeatureManager { get; } |
|||
|
|||
[NotNull] |
|||
protected GlobalFeatureDictionary AllFeatures { get; } |
|||
|
|||
protected GlobalModuleFeatures( |
|||
[NotNull] GlobalFeatureManager featureManager) |
|||
{ |
|||
FeatureManager = Check.NotNull(featureManager, nameof(featureManager)); |
|||
AllFeatures = new GlobalFeatureDictionary(); |
|||
} |
|||
|
|||
public virtual void Enable<TFeature>() |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
GetFeature<TFeature>().Enable(); |
|||
} |
|||
|
|||
public virtual void Disable<TFeature>() |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
GetFeature<TFeature>().Disable(); |
|||
} |
|||
|
|||
public virtual void SetEnabled<TFeature>(bool isEnabled) |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
GetFeature<TFeature>().SetEnabled(isEnabled); |
|||
} |
|||
|
|||
public virtual void Enable(string featureName) |
|||
{ |
|||
GetFeature(featureName).Enable(); |
|||
} |
|||
|
|||
public virtual void Disable(string featureName) |
|||
{ |
|||
GetFeature(featureName).Disable(); |
|||
} |
|||
|
|||
public virtual void SetEnabled(string featureName, bool isEnabled) |
|||
{ |
|||
GetFeature(featureName).SetEnabled(isEnabled); |
|||
} |
|||
|
|||
public virtual void EnableAll() |
|||
{ |
|||
foreach (var feature in AllFeatures.Values) |
|||
{ |
|||
feature.Enable(); |
|||
} |
|||
} |
|||
|
|||
public virtual void DisableAll() |
|||
{ |
|||
foreach (var feature in AllFeatures.Values) |
|||
{ |
|||
feature.Disable(); |
|||
} |
|||
} |
|||
|
|||
public virtual GlobalFeature GetFeature(string featureName) |
|||
{ |
|||
var feature = AllFeatures.GetOrDefault(featureName); |
|||
if (feature == null) |
|||
{ |
|||
throw new AbpException($"There is no feature defined by name '{featureName}'."); |
|||
} |
|||
|
|||
return feature; |
|||
} |
|||
|
|||
public virtual TFeature GetFeature<TFeature>() |
|||
where TFeature : GlobalFeature |
|||
{ |
|||
return (TFeature) GetFeature(GlobalFeatureNameAttribute.GetName<TFeature>()); |
|||
} |
|||
|
|||
public virtual IReadOnlyList<GlobalFeature> GetFeatures() |
|||
{ |
|||
return AllFeatures.Values.ToImmutableList(); |
|||
} |
|||
|
|||
protected void AddFeature(GlobalFeature feature) |
|||
{ |
|||
AllFeatures[feature.FeatureName] = feature; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Collections.Generic; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
public class GlobalModuleFeaturesDictionary : Dictionary<string, GlobalModuleFeatures> |
|||
{ |
|||
public GlobalFeatureManager FeatureManager { get; } |
|||
|
|||
public GlobalModuleFeaturesDictionary( |
|||
[NotNull] GlobalFeatureManager featureManager) |
|||
{ |
|||
FeatureManager = Check.NotNull(featureManager, nameof(featureManager)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Volo.Abp.GlobalFeatures |
|||
{ |
|||
[AttributeUsage(AttributeTargets.Class)] |
|||
public class RequiresGlobalFeatureAttribute : Attribute |
|||
{ |
|||
public Type Type { get; } |
|||
|
|||
public string Name { get; } |
|||
|
|||
public RequiresGlobalFeatureAttribute([NotNull] Type type) |
|||
{ |
|||
Type = Check.NotNull(type, nameof(type)); |
|||
} |
|||
|
|||
public RequiresGlobalFeatureAttribute([NotNull] string name) |
|||
{ |
|||
Name = Check.NotNullOrWhiteSpace(name, nameof(name)); |
|||
} |
|||
|
|||
public virtual string GetFeatureName() |
|||
{ |
|||
return Name ?? GlobalFeatureNameAttribute.GetName(Type); |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Quartz; |
|||
using Quartz.Spi; |
|||
|
|||
namespace Volo.Abp.Quartz |
|||
{ |
|||
/// <summary>
|
|||
/// Get the job from the dependency injection
|
|||
/// </summary>
|
|||
public class AbpQuartzJobFactory : IJobFactory |
|||
{ |
|||
private readonly IServiceProvider _serviceProvider; |
|||
|
|||
private readonly ConcurrentDictionary<IJob, IServiceScope> _scopes = new ConcurrentDictionary<IJob, IServiceScope>(); |
|||
|
|||
public AbpQuartzJobFactory(IServiceProvider serviceProvider) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
} |
|||
|
|||
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) |
|||
{ |
|||
var scope = _serviceProvider.CreateScope(); |
|||
var job = scope.ServiceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; |
|||
if (job == null) |
|||
{ |
|||
throw new ArgumentException("Given job does not implement IJob"); |
|||
} |
|||
_scopes.TryAdd(job, scope); |
|||
return job; |
|||
} |
|||
|
|||
public void ReturnJob(IJob job) |
|||
{ |
|||
_scopes.TryRemove(job, out var serviceScope); |
|||
serviceScope?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.GlobalFeatures; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures |
|||
{ |
|||
[GlobalFeatureName(Name)] |
|||
public class AbpAspNetCoreMvcTestFeature1 : Abp.GlobalFeatures.GlobalFeature |
|||
{ |
|||
public const string Name = "AbpAspNetCoreMvcTest.Feature1"; |
|||
|
|||
internal AbpAspNetCoreMvcTestFeature1([NotNull] AbpAspNetCoreMvcTestFeatures abpAspNetCoreMvcTestFeatures) |
|||
: base(abpAspNetCoreMvcTestFeatures) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public class AbpAspNetCoreMvcTestFeatures : GlobalModuleFeatures |
|||
{ |
|||
public const string ModuleName = "AbpAspNetCoreMvcTest"; |
|||
|
|||
public AbpAspNetCoreMvcTestFeature1 Reactions => GetFeature<AbpAspNetCoreMvcTestFeature1>(); |
|||
|
|||
public AbpAspNetCoreMvcTestFeatures([NotNull] GlobalFeatureManager featureManager) |
|||
: base(featureManager) |
|||
{ |
|||
AddFeature(new AbpAspNetCoreMvcTestFeature1(this)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.GlobalFeatures; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.GlobalFeatures |
|||
{ |
|||
[RequiresGlobalFeature("Not-Enabled-Feature")] |
|||
[Route("api/DisabledGlobalFeatureTestController-Test")] |
|||
public class DisabledGlobalFeatureTestController : AbpController |
|||
{ |
|||
[HttpGet] |
|||
[Route("TestMethod")] |
|||
public string TestMethod() |
|||
{ |
|||
return "TestMethod"; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
@page |
|||
@model Volo.Abp.AspNetCore.Mvc.GlobalFeatures.DisabledGlobalFeatureTestPage |
|||
|
|||
@{ |
|||
Layout = null; |
|||
} |
|||
|
|||
<!DOCTYPE html> |
|||
|
|||
<html> |
|||
<head> |
|||
<title></title> |
|||
</head> |
|||
<body> |
|||
<div> |
|||
|
|||
</div> |
|||
</body> |
|||
</html> |
|||