@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace AbpIoLocalization.Community.Localization |
|||
{ |
|||
[LocalizationResourceName("AbpIoCommunity")] |
|||
public class AbpIoCommunityResource |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Permission:CommunityArticle": "Community Article", |
|||
"Permission:Edit": "Edit", |
|||
"Waiting": "Waiting", |
|||
"Approved": "Approved", |
|||
"Rejected": "Rejected", |
|||
"Wait": "Wait", |
|||
"Approve": "Approve", |
|||
"Reject": "Reject", |
|||
"ReadArticle": "Read Article", |
|||
"Status": "Status", |
|||
"ContentSource": "Content Source", |
|||
"Details": "Details", |
|||
"Url": "Url", |
|||
"Title": "Title", |
|||
"CreationTime": "Creation time", |
|||
"Save": "Save", |
|||
"SameUrlAlreadyExist": "Same url already exist if you want to add this article, you should change the url!", |
|||
"UrlIsNotValid": "Url is not valid.", |
|||
"UrlNotFound" : "Url not found.", |
|||
"UrlContentNotFound": "Url content not found.", |
|||
"Summary": "Summary", |
|||
"MostRead": "Most Read", |
|||
"Latest": "Latest", |
|||
"ContributeAbpCommunity": "Contribute to the ABP Community", |
|||
"SubmitYourArticle": "Submit Your Article", |
|||
"ContributionGuide": "Contribution Guide", |
|||
"BugReport": "Bug Report", |
|||
"SeeAllArticles": "See All Articles", |
|||
"WelcomeToABPCommunity!": "Welcome to the ABP Community!", |
|||
"MyProfile": "My profile", |
|||
"MyOrganizations": "My organizations", |
|||
"EmailNotValid": "Please enter a valid email address.", |
|||
"FeatureRequest": "Feature Request", |
|||
"CreateArticleTitleInfo": "Title of the article to be shown on the article list.", |
|||
"CreateArticleUrlInfo": "Original GitHub/External URL of the article.", |
|||
"CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.", |
|||
"CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.", |
|||
"ThisExtensionIsNotAllowed": "This extension is not allowed.", |
|||
"TheFileIsTooLarge": "The file is too large.", |
|||
"GoToTheArticle": "Go to the Article", |
|||
"Contribute": "Contribute", |
|||
"OverallProgress": "Overall Progress", |
|||
"Done": "Done", |
|||
"Open": "Open", |
|||
"Closed": "Closed", |
|||
"LatestQuestionOnThe": "Latest Question On The", |
|||
"Stackoverflow": "Stackoverflow", |
|||
"Votes": "votes", |
|||
"Answer": "Answer", |
|||
"Views": "views", |
|||
"Answered": "Answered", |
|||
"WaitingForYourAnswer": "Waiting for your answer", |
|||
"Asked": "asked", |
|||
"AllQuestions": "All Questions", |
|||
"NextVersion": "Next Version", |
|||
"MilestoneErrorMessage": "Could not get the current milestone details from Github.", |
|||
"QuestionItemErrorMessage": "Could not get the latest question details from Stackoverflow.", |
|||
"Oops": "Oops!", |
|||
"CreateArticleSuccessMessage": "The Article has been successfully submitted. It will be published after a review from the site admin.", |
|||
"ChooseCoverImage": "Choose a cover image...", |
|||
"CoverImage": "Cover Image", |
|||
"ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!", |
|||
"Optional": "Optional", |
|||
"UpdateUserWebSiteInfo": "Example: https://johndoe.com", |
|||
"UpdateUserTwitterInfo": "Example: johndoe", |
|||
"UpdateUserGithubInfo": "Example: johndoe", |
|||
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...", |
|||
"UpdateUserCompanyInfo": "Example: Volosoft", |
|||
"UpdateUserJobTitleInfo": "Example: Software Developer", |
|||
"UserName": "UserName", |
|||
"Company": "Company", |
|||
"PersonalWebsite": "Personal Website", |
|||
"RegistrationDate": "Registration Date", |
|||
"Social": "Social", |
|||
"Biography": "Biography", |
|||
"HasNoPublishedArticlesYet": "has no published articles yet", |
|||
"Author": "Author", |
|||
"LatestGithubAnnouncements": "Latest Github Announcements", |
|||
"SeeAllAnnouncements": "See All Announcements", |
|||
"LatestBlogPost": "Latest Blog Post", |
|||
"Edit": "Edit", |
|||
"ProfileImageChange": "Change the profile image" |
|||
} |
|||
} |
|||
@ -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,32 @@ |
|||
# Social/External Logins |
|||
|
|||
The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. |
|||
|
|||
## Example: Facebook Authentication |
|||
|
|||
Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. |
|||
|
|||
#### Add the NuGet Package |
|||
|
|||
Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. |
|||
|
|||
#### Configure the Provider |
|||
|
|||
Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: |
|||
|
|||
````csharp |
|||
context.Services.AddAuthentication() |
|||
.AddFacebook(facebook => |
|||
{ |
|||
facebook.AppId = "..."; |
|||
facebook.AppSecret = "..."; |
|||
facebook.Scope.Add("email"); |
|||
facebook.Scope.Add("public_profile"); |
|||
}); |
|||
```` |
|||
|
|||
> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. |
|||
|
|||
## Angular UI |
|||
|
|||
Beginning from the v3.1, the Angular UI uses authorization code flow (as a best practice) to authenticate the user by redirecting to the MVC UI login page. So, even if you are using the Angular UI, social/external login integration is same as explained above and it will work out of the box. |
|||
@ -0,0 +1,263 @@ |
|||
# ABP Framework v3.1 RC Has Been Released |
|||
|
|||
Today, we are releasing the **ABP Framework version 3.1 Release Candidate** (RC). 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.0, 5.0...). |
|||
|
|||
### 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). |
|||
|
|||
## Breaking Changes / Special Notes |
|||
|
|||
### ABP & ABP Commercial |
|||
|
|||
* If you are using **EF Core**, you may need to **add a new migration** after upgrading the packages. Just run the standard "Add-Migration" command, check the generated migration code and execute the "Update-Database" command to apply changes to the database. |
|||
* If you have implemented **social/external logins** for your MVC / Razor Page UI application before, you may want to check [this issue](https://github.com/abpframework/abp/issues/4981). We made some improvements and changes that you may want to take action for your application. Beginning from v3.1, the users created their accounts via social login can still set a local password to login with local username/email & password. |
|||
|
|||
### ABP Commercial Only |
|||
|
|||
* We've **moved favicons** into `/wwwroot/images/favicon/` folder for the ASP.NET Core **MVC / Razor Page UI** applications. There are 10 favicon related files (including the `favicon.ico`) under this directory to better work with different browser and cases. You can create a new application to check this folder and copy the files into your own application. Then you can customize the icons for your own brand (hint: you can use a tool [like that](https://realfavicongenerator.net/) to create the favicons with various formats). |
|||
* Removed direct **Twitter & Facebook social login integrations** from the [account module](https://commercial.abp.io/modules/Volo.Account.Pro), for **MVC / Razor Pages UI**. Follow [this documentation](https://github.com/abpframework/abp/blob/dev/docs/en/Authentication/Social-External-Logins.md) to easily add social logins to your applications if you need. The account module provides all the infrastructure to handle social/external logins, you just need to configure it. |
|||
|
|||
## 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. |
|||
|
|||
### Module Entity Extensibility |
|||
|
|||
We had introduced a entity extension system that allows to add new properties to existing entities of depended modules by a simple configuration. When you add a new property, it appears on the create, edit and list views on the UI and created a new field in the related database table. We've implemented this system for the identity and tenant management modules, so you can extend entities of these modules. See [the documentation](https://github.com/abpframework/abp/blob/dev/docs/en/Module-Entity-Extensions.md). |
|||
|
|||
### 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,62 @@ |
|||
# ABP Framework 3.1 Final Has Been Released |
|||
|
|||
It is exciting for us to announce that we've released the ABP Framework & ABP Commercial 3.1 today. |
|||
|
|||
Since all the new features are already explained in details with the [3.1 RC Announcement Post](https://blog.abp.io/abp/ABP-Framework-v3.1-RC-Has-Been-Released), I will not repeat all the details here. Please read [the RC post](https://blog.abp.io/abp/ABP-Framework-v3.1-RC-Has-Been-Released) for **new feature and changes** you may need to do for your solution while upgrading to the version 3.1. |
|||
|
|||
## Creating New Solutions |
|||
|
|||
You can create a new solution with the ABP Framework version 3.1 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). |
|||
|
|||
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. |
|||
|
|||
## How to Upgrade an Existing Solution |
|||
|
|||
### Install/Update the ABP CLI |
|||
|
|||
First of all, install the ABP CLI or upgrade to the latest version. |
|||
|
|||
If you haven't installed yet: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
To update an existing installation: |
|||
|
|||
```bash |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
``` |
|||
|
|||
### ABP UPDATE Command |
|||
|
|||
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: |
|||
|
|||
````bash |
|||
abp update |
|||
```` |
|||
|
|||
After the update command, check [the RC blog post](https://blog.abp.io/abp/ABP-Framework-v3.1-RC-Has-Been-Released) to learn if you need to make any changes in your solution. |
|||
|
|||
> You may want to see the new [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). |
|||
|
|||
## About the version 3.2 |
|||
|
|||
The planned schedule for the version 3.2 is like that; |
|||
|
|||
* **September 17, 2020**: 3.2.0-rc.1 (release candidate) |
|||
* **October 1, 2020**: 3.2.0 final (stable) |
|||
|
|||
You can check [the GitHub milestone](https://github.com/abpframework/abp/milestone/39) to see the features/issues we are working on. |
|||
|
|||
## ABP Community & Articles |
|||
|
|||
We had lunched the [ABP Community web site](https://community.abp.io/) a few weeks before. The core ABP team and the ABP community have started to create content for the community. |
|||
|
|||
Here, the last three articles from the ABP Community: |
|||
|
|||
* [ABP Suite: How to Add the User Entity as a Navigation Property of Another Entity](https://community.abp.io/articles/abp-suite-how-to-add-the-user-entity-as-a-navigation-property-of-another-entity-furp75ex) by [@ebicoglu](https://github.com/ebicoglu) |
|||
* [Reuse ABP vNext Modules to Quickly Implement Application Features](https://community.abp.io/articles/reuse-abp-vnext-modules-to-quickly-implement-application-features-tdtmwd9w) by [@gdlcf88](https://github.com/gdlcf88) |
|||
* [Using DevExtreme Components With the ABP Framework](https://community.abp.io/articles/using-devextreme-components-with-the-abp-framework-zb8z7yqv) by [@cotur](https://github.com/cotur). |
|||
|
|||
We are looking for your contributions; You can [submit your article](https://community.abp.io/articles/submit)! We will promote your article to the community. |
|||
@ -0,0 +1,431 @@ |
|||
# Introducing the Angular Service Proxy Generation |
|||
|
|||
ABP Angular Service Proxy System **generates TypeScript services and models** to consume your backend HTTP APIs developed using the ABP Framework. So, you **don't manually create** models for your server side DTOs and perform raw HTTP calls to the server. |
|||
|
|||
ABP Framework has introduced the **new** Angular Service Proxy Generation system with the **version 3.1**. While this feature was available since the [v2.3](https://blog.abp.io/abp/ABP-Framework-v2_3_0-Has-Been-Released), it was not well covering some scenarios, like inheritance and generic types and had some known problems. **With the v3.1, we've re-written** it using the [Angular Schematics](https://angular.io/guide/schematics) system. Now, it is much more stable and feature rich. |
|||
|
|||
This post introduces the service proxy generation system and highlights some important features. |
|||
|
|||
## Installation |
|||
|
|||
### ABP CLI |
|||
|
|||
You need to have the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to use the system. So, install it if you haven't installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
If you already have installed it before, you can update to the latest version: |
|||
|
|||
````shell |
|||
dotnet tool update -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
### 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 the angular application: |
|||
|
|||
* 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 --save-dev |
|||
```` |
|||
|
|||
- Add `rootNamespace` entry into the `apis/default` section in the `/src/environments/environment.ts`, as shown below: |
|||
|
|||
```json |
|||
apis: { |
|||
default: { |
|||
... |
|||
rootNamespace: 'Acme.BookStore' |
|||
}, |
|||
} |
|||
``` |
|||
|
|||
`Acme.BookStore` should be replaced by the root namespace of your .NET project. This ensures to not create unnecessary nested folders while creating the service proxy code. This value is `AngularProxyDemo` for the example solution explained below. |
|||
|
|||
* Finally, add the following paths to the `tsconfig.base.json` to have a shortcut while importing proxies: |
|||
|
|||
```json |
|||
"paths": { |
|||
"@proxy": ["src/app/proxy/index.ts"], |
|||
"@proxy/*": ["src/app/proxy/*"] |
|||
} |
|||
``` |
|||
|
|||
## Basic Usage |
|||
|
|||
### Project Creation |
|||
|
|||
> If you already have a solution, you can skip this section. |
|||
|
|||
You need to [create](https://abp.io/get-started) your solution with the Angular UI. You can use the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to create a new solution: |
|||
|
|||
````bash |
|||
abp new AngularProxyDemo -u angular |
|||
```` |
|||
|
|||
#### Run the Application |
|||
|
|||
The backend application must be up and running to be able to use the service proxy code generation system. |
|||
|
|||
> See the [getting started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No) guide if you don't know details of creating and running the solution. |
|||
|
|||
### Backend |
|||
|
|||
Assume that we have an `IBookAppService` interface: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AngularProxyDemo.Books |
|||
{ |
|||
public interface IBookAppService : IApplicationService |
|||
{ |
|||
public Task<List<BookDto>> GetListAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
That uses a `BookDto` defined as shown: |
|||
|
|||
```csharp |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace AngularProxyDemo.Books |
|||
{ |
|||
public class BookDto : EntityDto<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
And implemented as the following: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AngularProxyDemo.Books |
|||
{ |
|||
public class BookAppService : ApplicationService, IBookAppService |
|||
{ |
|||
public async Task<List<BookDto>> GetListAsync() |
|||
{ |
|||
//TODO: get books from a database... |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
It simply returns a list of books. You probably want to get the books from a database, but it doesn't matter for this article. |
|||
|
|||
### HTTP API |
|||
|
|||
Thanks to the [auto API controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) system of the ABP Framework, we don't have to develop API controllers manually. Just **run the backend (*HttpApi.Host*) application** that shows the [Swagger UI](https://swagger.io/tools/swagger-ui/) by default. You will see the **GET** API for the books: |
|||
|
|||
 |
|||
|
|||
### Service Proxy Generation |
|||
|
|||
Open a **command line** in the **root folder of the Angular application** and execute the following command: |
|||
|
|||
````bash |
|||
abp generate-proxy |
|||
```` |
|||
|
|||
It should produce an output like the following: |
|||
|
|||
````bash |
|||
... |
|||
CREATE src/app/proxy/books/book.service.ts (446 bytes) |
|||
CREATE src/app/proxy/books/models.ts (148 bytes) |
|||
CREATE src/app/proxy/books/index.ts (57 bytes) |
|||
CREATE src/app/proxy/index.ts (33 bytes) |
|||
```` |
|||
|
|||
> `generate-proxy` command can take some some optional parameters for advanced scenarios (like [modular development](https://docs.abp.io/en/abp/latest/Module-Development-Basics)). You can take a look at the [documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Service-Proxies). |
|||
|
|||
#### The Generated Code |
|||
|
|||
`src/app/proxy/books/book.service.ts`: This is the service that can be injected and used to get the list of books; |
|||
|
|||
````js |
|||
import type { BookDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BookService { |
|||
apiName = 'Default'; |
|||
|
|||
getList = () => |
|||
this.restService.request<any, BookDto[]>({ |
|||
method: 'GET', |
|||
url: `/api/app/book`, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
```` |
|||
|
|||
`src/app/proxy/books/models.ts`: This file contains the modal classes corresponding to the DTOs defined in the server side; |
|||
|
|||
````js |
|||
import type { EntityDto } from '@abp/ng.core'; |
|||
|
|||
export interface BookDto extends EntityDto<string> { |
|||
name: string; |
|||
publishDate: string; |
|||
} |
|||
```` |
|||
|
|||
> There are a few more files have been generated to help you import the types easier. |
|||
|
|||
#### How to Import |
|||
|
|||
You can now import the `BookService` into any Angular component and use the `getList()` method to get the list of books. |
|||
|
|||
````js |
|||
import { BookService, BookDto } from '../proxy/books'; |
|||
```` |
|||
|
|||
You can also use the `@proxy` as a shortcut of the proxy folder: |
|||
|
|||
````js |
|||
import { BookService, BookDto } from '@proxy/books'; |
|||
```` |
|||
|
|||
### About the Generated Code |
|||
|
|||
The generated code is; |
|||
|
|||
* **Simple**: It is almost identical to the code if you've written it yourself. |
|||
* **Splitted**: Instead of a single, large file; |
|||
* It creates a separate `.ts` file for every backend **service**. **Model** (DTO) classes are also grouped per service. |
|||
* It understands the [modularity](https://docs.abp.io/en/abp/latest/Module-Development-Basics), so creates the services for your own **module** (or the module you've specified). |
|||
* **Object oriented**; |
|||
* Supports **inheritance** of server side DTOs and generates the code respecting to the inheritance structure. |
|||
* Supports **generic types**. |
|||
* Supports **re-using type definitions** across services and doesn't generate the same DTO multiple times. |
|||
* **Well-aligned to the backend**; |
|||
* Service **method signatures** match exactly with the services on the backend services. This is achieved by a special endpoint exposed by the ABP Framework that well defines the backend contracts. |
|||
* **Namespaces** are exactly matches to the backend services and DTOs. |
|||
* **Well-aligned with the ABP Framework**; |
|||
* Recognizes the **standard ABP Framework DTO types** (like `EntityDto`, `ListResultDto`... etc) and doesn't repeat these classes in the application code, but uses from the `@abp/ng.core` package. |
|||
* Uses the `RestService` defined by the `@abp/ng.core` package which simplifies the generated code, keeps it short and re-uses all the logics implemented by the `RestService` (including error handling, authorization token injection, using multiple server endpoints... etc). |
|||
|
|||
These are the main motivations behind the decision of creating a service proxy generation system, instead of using a pre-built tool like [NSWAG](https://github.com/RicoSuter/NSwag). |
|||
|
|||
## Other Examples |
|||
|
|||
Let me show you a few more examples. |
|||
|
|||
### Updating an Entity |
|||
|
|||
Assume that you added a new method to the server side application service, to update a book: |
|||
|
|||
```csharp |
|||
public Task<BookDto> UpdateAsync(Guid id, BookUpdateDto input); |
|||
``` |
|||
|
|||
`BookUpdateDto` is a simple class defined shown below: |
|||
|
|||
```csharp |
|||
using System; |
|||
|
|||
namespace AngularProxyDemo.Books |
|||
{ |
|||
public class BookUpdateDto |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public DateTime PublishDate { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Let's re-run the `generate-proxy` command: |
|||
|
|||
````bash |
|||
abp generate-proxy |
|||
```` |
|||
|
|||
This command will re-generate the proxies by updating some files. Let's see some of the changes; |
|||
|
|||
**book.service.ts** |
|||
|
|||
````js |
|||
import type { BookDto, BookUpdateDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class BookService { |
|||
apiName = 'Default'; |
|||
|
|||
getList = () => |
|||
this.restService.request<any, BookDto[]>({ |
|||
method: 'GET', |
|||
url: `/api/app/book`, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
update = (id: string, input: BookUpdateDto) => |
|||
this.restService.request<any, BookDto>({ |
|||
method: 'PUT', |
|||
url: `/api/app/book/${id}`, |
|||
body: input, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
```` |
|||
|
|||
`update` function has been added to the `BookService` that gets an `id` and a `BookUpdateDto` as the parameters. |
|||
|
|||
**models.ts** |
|||
|
|||
````typescript |
|||
import type { EntityDto } from '@abp/ng.core'; |
|||
|
|||
export interface BookDto extends EntityDto<string> { |
|||
name: string; |
|||
publishDate: string; |
|||
} |
|||
|
|||
export interface BookUpdateDto { |
|||
name: string; |
|||
publishDate: string; |
|||
} |
|||
```` |
|||
|
|||
Added a new DTO class: `BookUpdateDto`. |
|||
|
|||
### Advanced Example |
|||
|
|||
In this example, I want to show a DTO structure using inheritance, generics, arrays and dictionaries. |
|||
|
|||
I've created an `IOrderAppService` as shown below: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AngularProxyDemo.Orders |
|||
{ |
|||
public interface IOrderAppService : IApplicationService |
|||
{ |
|||
public Task CreateAsync(OrderCreateDto input); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`OrderCreateDto` and the related DTOs are as the followings; |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace AngularProxyDemo.Orders |
|||
{ |
|||
public class OrderCreateDto : IHasExtraProperties |
|||
{ |
|||
public Guid CustomerId { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
|
|||
//ARRAY of DTOs |
|||
public OrderDetailDto[] Details { get; set; } |
|||
|
|||
//DICTIONARY |
|||
public Dictionary<string, object> ExtraProperties { get; set; } |
|||
} |
|||
|
|||
public class OrderDetailDto : GenericDetailDto<int> //INHERIT from GENERIC |
|||
{ |
|||
public string Note { get; set; } |
|||
} |
|||
|
|||
//GENERIC class |
|||
public abstract class GenericDetailDto<TCount> |
|||
{ |
|||
public Guid ProductId { get; set; } |
|||
|
|||
public TCount Count { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
When I run the `abp generate-proxy` command again, I see there are some created and updated files. Let's see some important ones; |
|||
|
|||
`src/app/proxy/orders/order.service.ts` |
|||
|
|||
````js |
|||
import type { OrderCreateDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class OrderService { |
|||
apiName = 'Default'; |
|||
|
|||
create = (input: OrderCreateDto) => |
|||
this.restService.request<any, void>({ |
|||
method: 'POST', |
|||
url: `/api/app/order`, |
|||
body: input, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
```` |
|||
|
|||
`src/app/proxy/orders/models.ts` |
|||
|
|||
````typescript |
|||
export interface GenericDetailDto<TCount> { |
|||
productId: string; |
|||
count: TCount; |
|||
} |
|||
|
|||
export interface OrderCreateDto { |
|||
customerId: string; |
|||
creationTime: string; |
|||
details: OrderDetailDto[]; |
|||
extraProperties: Record<string, object>; |
|||
} |
|||
|
|||
export interface OrderDetailDto extends GenericDetailDto<number> { |
|||
note: string; |
|||
} |
|||
```` |
|||
|
|||
## Conclusion |
|||
|
|||
`abp generate-proxy` is a very handy command that creates all the necessary code to consume your ABP based backend HTTP APIs. It generates a clean code that is well aligned to the backend services and benefits from the power of TypeScript (by using generics, inheritance...). |
|||
|
|||
## The Documentation |
|||
|
|||
See [the documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Service-Proxies) for details of the Angular Service Proxy Generation. |
|||
|
After Width: | Height: | Size: 9.3 KiB |
@ -0,0 +1,462 @@ |
|||
# Real Time Messaging In A Distributed Architecture Using Abp Framework, SingalR & RabbitMQ |
|||
|
|||
In this article, we will build a basic real time messaging application in a distributed architecture. We will use [Abp Framework](https://abp.io) for infrastructure and tiered startup template, [SignalR](https://dotnet.microsoft.com/apps/aspnet/signalr) for real time server-client communication and [RabbitMQ](https://www.rabbitmq.com/) as the distributed event bus. |
|||
|
|||
When Web & API tiers are separated, it is impossible to directly send a server-to-client message from the HTTP API. This is also true for a microservice architected application. We suggest to use the distributed event bus to deliver the message from API application to the web application, then to the client. |
|||
|
|||
|
|||
 |
|||
|
|||
Above, you can see the data-flow that we will implement in this article. This diagram represents how data will flow in our application when **Client 1** sends a message to **Client 2**. It is explained in 5 steps: |
|||
|
|||
1. **Client 1** sends a message data to **Web Application** via REST call. |
|||
2. **Web Application** redirects the message data to **Http Api**. |
|||
3. The message data is processed in **Http Api** and **Http Api** publishes an event that holds the data that will be sent to **Client 2**. |
|||
4. **Web application**, that is subscribed to that event, receives it. |
|||
5. **Web Application** sends the message to **Client 2**. |
|||
|
|||
For this example flow, we could send message from **Client 1** to **Client 2** directly on the **SignalR Hub**. However, what we are trying here to demonstrate is sending a real-time message from the **Http Api** to a specific user who is connected to the web application. |
|||
|
|||
## Implementation |
|||
|
|||
### Startup template and initial run |
|||
|
|||
[Abp Framework](https://www.abp.io) offers startup templates to get into the business faster. We can download a new tiered startup template using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): |
|||
|
|||
`abp new SignalRTieredDemo --tiered` |
|||
|
|||
After download is finished, we run ***.DbMigrator** project to create the database and seed initial data (admin user, role etc). Then we run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** to see our application working. |
|||
|
|||
### Creating Application Layer |
|||
|
|||
We create an [application service](https://docs.abp.io/en/abp/latest/Application-Services) that publishes the message as event. |
|||
|
|||
In ***.Application.Contracts** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public interface IChatAppService : IApplicationService |
|||
{ |
|||
Task SendMessageAsync(SendMessageInput input); |
|||
} |
|||
} |
|||
```` |
|||
Input DTO for SendMessageAsync method: |
|||
|
|||
````csharp |
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class SendMessageInput |
|||
{ |
|||
public string TargetUserName { get; set; } |
|||
|
|||
public string Message { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
Event transfer object (ETO) for communication on event bus: |
|||
|
|||
````csharp |
|||
using System; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class ReceivedMessageEto |
|||
{ |
|||
public string ReceivedText { get; set; } |
|||
|
|||
public Guid TargetUserId { get; set; } |
|||
|
|||
public string SenderUserName { get; set; } |
|||
|
|||
public ReceivedMessageEto( |
|||
Guid targetUserId, string senderUserName, string receivedText) |
|||
{ |
|||
ReceivedText = receivedText; |
|||
TargetUserId = targetUserId; |
|||
SenderUserName = senderUserName; |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
In ***.Application** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace SignalRTieredDemo |
|||
{ |
|||
public class ChatAppService: SignalRTieredDemoAppService, IChatAppService |
|||
{ |
|||
private readonly IIdentityUserRepository _identityUserRepository; |
|||
private readonly ILookupNormalizer _lookupNormalizer; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public ChatAppService(IIdentityUserRepository identityUserRepository, ILookupNormalizer lookupNormalizer, IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_identityUserRepository = identityUserRepository; |
|||
_lookupNormalizer = lookupNormalizer; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async Task SendMessageAsync(SendMessageInput input) |
|||
{ |
|||
var targetId = (await _identityUserRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.NormalizeName(input.TargetUserName))).Id; |
|||
|
|||
await _distributedEventBus.PublishAsync(new ReceivedMessageEto(targetId, CurrentUser.UserName, input.Message)); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Creating API Layer |
|||
|
|||
We create an endpoint for sending message that redirects the process to application layer: |
|||
|
|||
In **controllers** folder of ***.HttpApi** project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace SignalRTieredDemo.Controllers |
|||
{ |
|||
[Route("api/app/chat")] |
|||
public class ChatController : AbpController, IChatAppService |
|||
{ |
|||
private readonly IChatAppService _chatAppService; |
|||
|
|||
public ChatController(IChatAppService chatAppService) |
|||
{ |
|||
_chatAppService = chatAppService; |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("send-message")] |
|||
public async Task SendMessageAsync(SendMessageInput input) |
|||
{ |
|||
await _chatAppService.SendMessageAsync(input); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
### Adding SignalR |
|||
|
|||
To add SignalR to our solution, we add `Volo.Abp.AspNetCore.SignalR` nuget package to ***.Web** project. |
|||
|
|||
And then add `AbpAspNetCoreSignalRModule` dependency: |
|||
|
|||
````csharp |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
[DependsOn( |
|||
... |
|||
typeof(AbpAspNetCoreSignalRModule) // <--- |
|||
)] |
|||
public class SignalRTieredDemoWebModule : AbpModule |
|||
{ |
|||
```` |
|||
|
|||
Also, we need to add [@abp/signalr](https://www.npmjs.com/package/@abp/signalr) npm package to package.json in ***.Web** project, then run **yarn** and **gulp** commands. |
|||
|
|||
`````json |
|||
{ |
|||
. |
|||
. |
|||
"dependencies": { |
|||
. |
|||
. |
|||
"@abp/signalr": "^2.9.0" |
|||
} |
|||
} |
|||
````` |
|||
|
|||
*Remember to add the latest package version.* |
|||
|
|||
You can find more information for Abp SignalR Integration on [the related document](https://docs.abp.io/en/abp/latest/SignalR-Integration). |
|||
|
|||
### Creating A Hub |
|||
|
|||
We need a hub for SignalR connection. We can inherit it from `AbpHup` base class. |
|||
|
|||
In ***.Web** project: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.AspNetCore.SignalR; |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
[Authorize] |
|||
public class ChatHub : AbpHub |
|||
{ |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
While you could inherit from the standard `Hub` class, `AbpHub` has some common services pre-injected as base properties, which is useful on your development. |
|||
|
|||
### Adding & Configuring RabbitMQ |
|||
|
|||
To add RabbitMQ to our solution, we add `Volo.Abp.EventBus.RabbitMQ` nuget package to ***.HttpApi.Host** and ***.Web** projects. |
|||
|
|||
Launch a **command line**, navigate to directory where ***.HttpApi.Host.csproj** file exist, and run the command below using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.EventBus.RabbitMQ |
|||
```` |
|||
|
|||
Then do the same for ***.Web** project. |
|||
|
|||
After we add the package, we configure RabbitMQ by adding configuration in **appsettings.json** files of those projects. |
|||
|
|||
For ***.HttpApi.Host** project: |
|||
|
|||
````json |
|||
{ |
|||
... |
|||
"RabbitMQ": { |
|||
"Connections": { |
|||
"Default": { |
|||
"HostName": "localhost" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"ClientName": "SignalRTieredDemo_HttpApi", |
|||
"ExchangeName": "SignalRTieredDemoTest" |
|||
} |
|||
}, |
|||
... |
|||
} |
|||
```` |
|||
For ***.Web** project: |
|||
````json |
|||
{ |
|||
... |
|||
"RabbitMQ": { |
|||
"Connections": { |
|||
"Default": { |
|||
"HostName": "localhost" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"ClientName": "SignalRTieredDemo_Web", |
|||
"ExchangeName": "SignalRTieredDemoTest" |
|||
} |
|||
}, |
|||
... |
|||
} |
|||
|
|||
```` |
|||
|
|||
### Handling New Message Event |
|||
|
|||
Once we publish a new message event from `Http Api`, we must to handle it in `Web Application`. Therefore we need an event handler in ***.Web** Project: |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.SignalR; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace SignalRTieredDemo.Web |
|||
{ |
|||
public class ReceivedMessageEventHandler : |
|||
IDistributedEventHandler<ReceivedMessageEto>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IHubContext<ChatHub> _hubContext; |
|||
|
|||
public ReceivedMessageEventHandler(IHubContext<ChatHub> hubContext) |
|||
{ |
|||
_hubContext = hubContext; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(ReceivedMessageEto eto) |
|||
{ |
|||
var message = $"{eto.SenderUserName}: {eto.ReceivedText}"; |
|||
|
|||
await _hubContext.Clients |
|||
.User(eto.TargetUserId.ToString()) |
|||
.SendAsync("ReceiveMessage", message); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Creating Chat Page |
|||
|
|||
We create the files below in **Pages** folder of ***.Web** Project. |
|||
|
|||
**Chat.cshtml**: |
|||
|
|||
````html |
|||
@page |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR |
|||
@model SignalRTieredDemo.Web.Pages.ChatModel |
|||
@section styles { |
|||
<abp-style src="/Pages/Chat.css" /> |
|||
} |
|||
@section scripts { |
|||
<abp-script type="typeof(SignalRBrowserScriptContributor)" /> |
|||
<abp-script src="/Pages/Chat.js" /> |
|||
} |
|||
<h1>Chat</h1> |
|||
|
|||
<div> |
|||
<abp-row> |
|||
<abp-column size-md="_6"> |
|||
<div>All Messages:</div> |
|||
<ul id="MessageList" style=""> |
|||
</ul> |
|||
</abp-column> |
|||
<abp-column size-md="_6"> |
|||
<form> |
|||
<abp-row> |
|||
<abp-column> |
|||
<label for="TargetUser">Target user:</label> |
|||
<input type="text" id="TargetUser" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
<abp-row class="mt-2"> |
|||
<abp-column> |
|||
<label for="Message">Message:</label> |
|||
<textarea id="Message" rows="4"></textarea> |
|||
</abp-column> |
|||
</abp-row> |
|||
<abp-row class="mt-2"> |
|||
<abp-column> |
|||
<abp-button type="submit" id="SendMessageButton" button-type="Primary" size="Block" text="SEND!" /> |
|||
</abp-column> |
|||
</abp-row> |
|||
</form> |
|||
</abp-column> |
|||
</abp-row> |
|||
</div> |
|||
```` |
|||
|
|||
**Chat.cshtml.cs**: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
|
|||
namespace SignalRTieredDemo.Web.Pages |
|||
{ |
|||
public class ChatModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**Chat.css**: |
|||
|
|||
````css |
|||
#MessageList { |
|||
border: 1px solid gray; |
|||
height: 400px; |
|||
overflow: auto; |
|||
list-style: none; |
|||
padding-left: 0; |
|||
padding: 10px; |
|||
} |
|||
|
|||
#TargetUser { |
|||
width: 100%; |
|||
} |
|||
|
|||
#Message { |
|||
width: 100%; |
|||
} |
|||
```` |
|||
|
|||
**Chat.js**: |
|||
|
|||
````javascript |
|||
$(function () { |
|||
var connection = new signalR.HubConnectionBuilder().withUrl("/signalr-hubs/chat").build(); |
|||
|
|||
connection.on("ReceiveMessage", function (message) { |
|||
console.log(message); |
|||
$('#MessageList').append('<li><strong><i class="fas fa-long-arrow-alt-right"></i> ' + message + '</strong></li>'); |
|||
}); |
|||
|
|||
connection.start().then(function () { |
|||
|
|||
}).catch(function (err) { |
|||
return console.error(err.toString()); |
|||
}); |
|||
|
|||
$('#SendMessageButton').click(function (e) { |
|||
e.preventDefault(); |
|||
|
|||
var targetUserName = $('#TargetUser').val(); |
|||
var message = $('#Message').val(); |
|||
$('#Message').val(''); |
|||
|
|||
|
|||
signalRTieredDemo.controllers.chat.sendMessage({ |
|||
targetUserName: targetUserName, |
|||
message: message |
|||
}).then(function() { |
|||
$('#MessageList') |
|||
.append('<li><i class="fas fa-long-arrow-alt-left"></i> ' + abp.currentUser.userName + ': ' + message + '</li>'); |
|||
}); |
|||
|
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then we can add this new page to menu on ***MenuContributor.cs** in **Menus** folder: |
|||
|
|||
````csharp |
|||
... |
|||
public class SignalRTieredDemoMenuContributor : IMenuContributor |
|||
{ |
|||
... |
|||
private Task ConfigureMainMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
... |
|||
context.Menu.Items.Add(new ApplicationMenuItem("SignalRDemo.Chat", "Chat", "/Chat")); // <-- We add this line |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
... |
|||
} |
|||
```` |
|||
|
|||
## Running & Testing |
|||
|
|||
We run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** in order. After ***.Web** project is ran, firstly login with `admin` username and `1q2w3E*` password. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
After we login, go to `/Identity/Users` page and create a new user. So that we can chat with them. |
|||
|
|||
 |
|||
|
|||
Then we open the application in another browser and login with the user we created above. Now we can go to chat page and start messaging: |
|||
|
|||
 |
|||
|
|||
We can test with more user. All sent and incoming messages are displayed in the left box. |
|||
|
|||
### Source code |
|||
|
|||
Source code of the final application can be found on the [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo). |
|||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 39 KiB |
@ -0,0 +1,396 @@ |
|||
# File Upload/Download with BLOB Storage System in ASP.NET Core & ABP Framework |
|||
|
|||
## Introduction |
|||
|
|||
This step-by-step article describes how to upload a file to a Web server and also download by client with using ASP.NET Core & ABP Framework. By following this article, you will create a web project and its related code to upload and download files. |
|||
|
|||
Before the creating application, we need to know some fundamentals. |
|||
|
|||
## BLOB Storing |
|||
|
|||
It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. |
|||
|
|||
A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. |
|||
|
|||
The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; |
|||
|
|||
- You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. |
|||
- You can then **easily change** your BLOB storage without changing your application code. |
|||
- If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. |
|||
|
|||
ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy). |
|||
|
|||
To get more information about ABP BLOB Storing system, please check this [documentation](https://docs.abp.io/en/abp/latest/Blob-Storing). |
|||
|
|||
## 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: |
|||
|
|||
`abp new FileActionsDemo -m none` |
|||
|
|||
After the download is finished, we run `FileActionsDemo.DbMigrator` project to create the database and seed initial data (admin user, role, etc). Then we run `FileActionsDemo.Web` to see our application working. |
|||
|
|||
> _Default admin username is **admin** and password is **1q2w3E\***_ |
|||
|
|||
 |
|||
|
|||
### Adding Blob Storing Module |
|||
|
|||
For this article, we use [Blob Storing Database Provider](https://docs.abp.io/en/abp/latest/Blob-Storing-Database). |
|||
|
|||
You can use [Azure](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure) or [File System](https://docs.abp.io/en/abp/latest/Blob-Storing-File-System) providers also. |
|||
|
|||
Open a command prompt (terminal) in the folder containing your solution (.sln) file and run the following command: |
|||
|
|||
`abp add-module Volo.Abp.BlobStoring.Database` |
|||
|
|||
This action will add the module depencies and also module migration. After this action, run `FileActionsDemo.DbMigrator` to update the database. |
|||
|
|||
### Setting up Blob Storaging |
|||
|
|||
BLOB Strorage system works with `Containers`. Before the using blob storage, we need to create our blob container. |
|||
|
|||
Create a class that name `MyFileContainer` at the `FileActionsDemo.Domain` project. |
|||
|
|||
```csharp |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
[BlobContainerName("my-file-container")] |
|||
public class MyFileContainer |
|||
{ |
|||
|
|||
} |
|||
} |
|||
``` |
|||
|
|||
That's all, we can start to use BLOB storing in our application. |
|||
|
|||
## Creating Application Layer |
|||
|
|||
Before the creating Application Service, we need to create some [DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)s that used by Application Service. |
|||
|
|||
Create following DTOs in `FileActionsDemo.Application.Contracts` project. |
|||
|
|||
- `BlobDto.cs` |
|||
|
|||
```csharp |
|||
namespace FileActionsDemo |
|||
{ |
|||
public class BlobDto |
|||
{ |
|||
public byte[] Content { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `GetBlobRequestDto.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class GetBlobRequestDto |
|||
{ |
|||
[Required] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- `SaveBlobInputDto.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class SaveBlobInputDto |
|||
{ |
|||
public byte[] Content { get; set; } |
|||
|
|||
[Required] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Create `IFileAppService.cs` interface at the same place with DTOs. |
|||
|
|||
- `IFileAppService` |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public interface IFileAppService : IApplicationService |
|||
{ |
|||
Task SaveBlobAsync(SaveBlobInputDto input); |
|||
|
|||
Task<BlobDto> GetBlobAsync(GetBlobRequestDto input); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
After creating DTOs and interface, `FileActionsDemo.Application.Contracts` project should be like as following image. |
|||
|
|||
 |
|||
|
|||
Then we can create our Application Service. |
|||
|
|||
Create `FileAppService.cs` in `FileActionsDemo.Application` project. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class FileAppService : ApplicationService, IFileAppService |
|||
{ |
|||
private readonly IBlobContainer<MyFileContainer> _fileContainer; |
|||
|
|||
public FileAppService(IBlobContainer<MyFileContainer> fileContainer) |
|||
{ |
|||
_fileContainer = fileContainer; |
|||
} |
|||
|
|||
public async Task SaveBlobAsync(SaveBlobInputDto input) |
|||
{ |
|||
await _fileContainer.SaveAsync(input.Name, input.Content, true); |
|||
} |
|||
|
|||
public async Task<BlobDto> GetBlobAsync(GetBlobRequestDto input) |
|||
{ |
|||
var blob = await _fileContainer.GetAllBytesAsync(input.Name); |
|||
|
|||
return new BlobDto |
|||
{ |
|||
Name = input.Name, |
|||
Content = blob |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see in previous code block, we inject `IBlobContainer<MyFileContainer>` to our app service. It will handle all blob actions for us. |
|||
|
|||
- `SaveBlobAsync` method uses `SaveAsync` of `IBlobContainer<MyFileContainer>` to save the given blob to storage, this is a simple example so we don't check is there any file exist with same name. We sent blob name, blob content and `true` for `overrideExisting` parameter. |
|||
|
|||
- `GetBlobAsync` method is uses `GetAllBytesAsync` of `IBlobContainer<MyFileContainer>` to get blob content by name. |
|||
|
|||
We finished the application layer for this project. After that we will create a `Controller` for API and `Razor Page` for UI. |
|||
|
|||
## Creating Controller |
|||
|
|||
Create `FileController.cs` in your `FileActionsDemo.HttpApi` project. |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace FileActionsDemo |
|||
{ |
|||
public class FileController : AbpController |
|||
{ |
|||
private readonly IFileAppService _fileAppService; |
|||
|
|||
public FileController(IFileAppService fileAppService) |
|||
{ |
|||
_fileAppService = fileAppService; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("download/{fileName}")] |
|||
public async Task<IActionResult> DownloadAsync(string fileName) |
|||
{ |
|||
var fileDto = await _fileAppService.GetBlobAsync(new GetBlobRequestDto{ Name = fileName }); |
|||
|
|||
return File(fileDto.Content, "application/octet-stream", fileDto.Name); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see, `FileController` injects `IFileAppService` that we defined before. This controller has only one endpoint. |
|||
|
|||
`DownloadAsync` is using to send file from server to client. |
|||
|
|||
This endpoint is requires only a `string` parameter, then we use that parameter to get stored blob. If blob is exist, we return a `File` result so download process can start. |
|||
|
|||
## Creating User Interface |
|||
|
|||
We will create only one page to prove download and upload actions are working. |
|||
|
|||
Create folder that name `Files` in your `Pages` folder at `FileActionsDemo.Web` project. |
|||
|
|||
Create a Razor page that name `Index` with its model. |
|||
|
|||
- `Index.cshtml.cs` |
|||
|
|||
```csharp |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Http; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace FileActionsDemo.Web.Pages.Files |
|||
{ |
|||
public class Index : AbpPageModel |
|||
{ |
|||
[BindProperty] |
|||
public UploadFileDto UploadFileDto { get; set; } |
|||
|
|||
private readonly IFileAppService _fileAppService; |
|||
|
|||
public bool Uploaded { get; set; } = false; |
|||
|
|||
public Index(IFileAppService fileAppService) |
|||
{ |
|||
_fileAppService = fileAppService; |
|||
} |
|||
|
|||
public void OnGet() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public async Task<IActionResult> OnPostAsync() |
|||
{ |
|||
using (var memoryStream = new MemoryStream()) |
|||
{ |
|||
await UploadFileDto.File.CopyToAsync(memoryStream); |
|||
|
|||
await _fileAppService.SaveBlobAsync( |
|||
new SaveBlobInputDto |
|||
{ |
|||
Name = UploadFileDto.Name, |
|||
Content = memoryStream.ToArray() |
|||
} |
|||
); |
|||
} |
|||
|
|||
return Page(); |
|||
} |
|||
} |
|||
|
|||
public class UploadFileDto |
|||
{ |
|||
[Required] |
|||
[Display(Name = "File")] |
|||
public IFormFile File { get; set; } |
|||
|
|||
[Required] |
|||
[Display(Name = "Filename")] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
``` |
|||
|
|||
As you see, we use `UploadFileDto` as a `BindProperty` and we inject `IFileAppService` to upload files. |
|||
|
|||
The `UploadFileDto` is requires a `string` parameter for using as a blob name and a `IFormFile` that sent by user. |
|||
|
|||
At the post action (`OnPostAsync`), if everything is well, we use `MemoryStream` to get all bytes from file content. |
|||
|
|||
Then we save file with `SaveBlobAsync` method of `IFileAppService`. |
|||
|
|||
- `Index.cshtml` |
|||
|
|||
```csharp |
|||
@page |
|||
@model FileActionsDemo.Web.Pages.Files.Index |
|||
|
|||
@section scripts{ |
|||
<abp-script src="/Pages/Files/index.js" /> |
|||
} |
|||
|
|||
<abp-card> |
|||
<abp-card-header> |
|||
<h3>File Upload and Download</h3> |
|||
</abp-card-header> |
|||
<abp-card-body> |
|||
<abp-row> |
|||
<abp-column> |
|||
<h3>Upload File</h3> |
|||
<hr /> |
|||
<form method="post" enctype="multipart/form-data"> |
|||
<abp-input asp-for="UploadFileDto.Name"></abp-input> |
|||
|
|||
<abp-input asp-for="UploadFileDto.File"></abp-input> |
|||
|
|||
<input type="submit" class="btn btn-info" /> |
|||
</form> |
|||
</abp-column> |
|||
|
|||
<abp-column style="border-left: 1px dotted gray"> |
|||
<h3>Download File</h3> |
|||
<hr /> |
|||
<form id="DownloadFile"> |
|||
<div class="form-group"> |
|||
<label for="fileName">Filename</label><span> * </span> |
|||
<input type="text" id="fileName" name="fileName" class="form-control "> |
|||
</div> |
|||
|
|||
<input type="submit" class="btn btn-info"/> |
|||
</form> |
|||
</abp-column> |
|||
</abp-row> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
|
|||
We divided the page vertically, left side will be using for upload and right side will be using for download. We use [ABP Tag Helpers](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Index) to create page. |
|||
|
|||
- `index.js` |
|||
|
|||
```javascript |
|||
$(function () { |
|||
var DOWNLOAD_ENDPOINT = "/download"; |
|||
|
|||
var downloadForm = $("form#DownloadFile"); |
|||
|
|||
downloadForm.submit(function (event) { |
|||
event.preventDefault(); |
|||
|
|||
var fileName = $("#fileName").val().trim(); |
|||
|
|||
var downloadWindow = window.open( |
|||
DOWNLOAD_ENDPOINT + "/" + fileName, |
|||
"_blank" |
|||
); |
|||
downloadWindow.focus(); |
|||
}); |
|||
|
|||
$("#UploadFileDto_File").change(function () { |
|||
var fileName = $(this)[0].files[0].name; |
|||
|
|||
$("#UploadFileDto_Name").val(fileName); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This jQuery codes are using for download. Also we wrote a simple code to autofill `Filename` input when user selects a file. |
|||
|
|||
After creating razor page and js file, `FileActionsDemo.Web` project should be like as following image. |
|||
|
|||
 |
|||
|
|||
## Result |
|||
|
|||
After completing code tutorial, run `FileActionsDemo.Web` project and go `/Files`. You can upload any file with any name and also download those uploaded files. |
|||
|
|||
 |
|||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@ -0,0 +1,275 @@ |
|||
# Implementing Passwordless Authentication in ASP.NET Core Identity |
|||
|
|||
## Introduction |
|||
|
|||
In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering the password. |
|||
|
|||
This can be useful especially if you want to make someone login to the application with your user, without sharing your secret password. The generated link will be for a single use. |
|||
|
|||
### Source Code |
|||
|
|||
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
|||
|
|||
## Creating the Solution |
|||
|
|||
Before starting the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No). |
|||
|
|||
## Step-1 |
|||
|
|||
Create a class named **PasswordlessLoginProvider** in your ***.Web** project: |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web |
|||
{ |
|||
public class PasswordlessLoginProvider<TUser> : TotpSecurityStampBasedTokenProvider<TUser> |
|||
where TUser : class |
|||
{ |
|||
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user) |
|||
{ |
|||
return Task.FromResult(false); |
|||
} |
|||
|
|||
//We need to override this method as well. |
|||
public override async Task<string> GetUserModifierAsync(string purpose, UserManager<TUser> manager, TUser user) |
|||
{ |
|||
var userId = await manager.GetUserIdAsync(user); |
|||
|
|||
return "PasswordlessLogin:" + purpose + ":" + userId; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
## Step-2 |
|||
|
|||
Create **IdentityBuilderExtensions.cs** in your ***.Web** project. We will use this extension method in the `ConfigureServices`. |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web |
|||
{ |
|||
public static class IdentityBuilderExtensions |
|||
{ |
|||
public static IdentityBuilder AddPasswordlessLoginProvider(this IdentityBuilder builder) |
|||
{ |
|||
var userType = builder.UserType; |
|||
var totpProvider = typeof(PasswordlessLoginProvider<>).MakeGenericType(userType); |
|||
return builder.AddTokenProvider("PasswordlessLoginProvider", totpProvider); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
## Step-3 |
|||
|
|||
Add the token provider to the `Identity` middleware. To do this, find the module class (eg: `PasswordlessAuthenticationWebModule.cs` in here) in your ***.Web** project and add the below into the `ConfigureServices()` method. |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
context.Services |
|||
.GetObject<IdentityBuilder>() |
|||
.AddDefaultTokenProviders() |
|||
.AddPasswordlessLoginProvider(); |
|||
} |
|||
``` |
|||
|
|||
## Step-4 |
|||
|
|||
We need to create a user interface to be able to generate the magic login link. To do this quickly, open your existing **Index.cshtml.cs** in your ***.Web** project. It's under `Pages` folder. And copy-paste the below content. |
|||
|
|||
**Index.cshtml.cs** |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace PasswordlessAuthentication.Web.Pages |
|||
{ |
|||
public class IndexModel : PasswordlessAuthenticationPageModel |
|||
{ |
|||
protected IdentityUserManager UserManager { get; } |
|||
|
|||
private readonly IIdentityUserRepository _userRepository; |
|||
|
|||
public string PasswordlessLoginUrl { get; set; } |
|||
|
|||
public string Email { get; set; } |
|||
|
|||
public IndexModel(IdentityUserManager userManager, IIdentityUserRepository userRepository) |
|||
{ |
|||
UserManager = userManager; |
|||
_userRepository = userRepository; |
|||
} |
|||
|
|||
public ActionResult OnGet() |
|||
{ |
|||
if (!CurrentUser.IsAuthenticated) |
|||
{ |
|||
return Redirect("/Account/Login"); |
|||
} |
|||
|
|||
return Page(); |
|||
} |
|||
|
|||
//added for passwordless authentication |
|||
public async Task<IActionResult> OnPostGeneratePasswordlessTokenAsync() |
|||
{ |
|||
var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); |
|||
|
|||
var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", |
|||
"passwordless-auth"); |
|||
|
|||
PasswordlessLoginUrl = Url.Action("Login", "Passwordless", |
|||
new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); |
|||
|
|||
return Page(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
We added `OnPostGeneratePasswordlessTokenAsync()` action to generate the link. We will generate a link for the **admin** user. Therefore, we injected `IIdentityUserRepository` to get admin user Id. Using the `UserManager.GenerateUserTokenAsync()` method, we generated a token. After that, we created the URL with the admin user Id and the token. Now we will show the `PasswordlessLoginUrl` on the page. |
|||
|
|||
## Step-5 |
|||
|
|||
Create a class named **PasswordlessAuthenticationMenus** under `Menus` folder in your ***.Web** project. And set the content as below. |
|||
|
|||
```csharp |
|||
namespace PasswordlessAuthentication.Web.Menus |
|||
{ |
|||
public class PasswordlessAuthenticationMenus |
|||
{ |
|||
public const string GroupName = "PasswordlessAuthentication"; |
|||
|
|||
public const string Home = GroupName + ".Home"; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Step-6 |
|||
|
|||
Open your **Index.cshtml** and set the content as below. We added a form that posts to `GeneratePasswordlessToken` action in the razor page. And it will set the `PasswordlessLoginUrl` field. |
|||
|
|||
```html |
|||
@page |
|||
@using MyBookStore.Web.Menus |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
|||
@model MyBookStore.Web.Pages.IndexModel |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using MyBookStore.Localization |
|||
@inject IHtmlLocalizer<MyBookStoreResource> L |
|||
@{ |
|||
ViewBag.PageTitle = "Home"; |
|||
} |
|||
@inject IPageLayout PageLayout |
|||
@{ |
|||
PageLayout.Content.Title = L["Home"].Value; |
|||
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); |
|||
PageLayout.Content.MenuItemName = MyBookStoreMenus.Home; |
|||
} |
|||
<abp-card> |
|||
<abp-card-body> |
|||
<form asp-page-handler="GeneratePasswordlessToken" method="post"> |
|||
|
|||
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button> |
|||
|
|||
@if (Model.PasswordlessLoginUrl != null) |
|||
{ |
|||
<abp-card class="mt-3 p-3"> |
|||
<a href="@Model.PasswordlessLoginUrl">@Model.PasswordlessLoginUrl</a> |
|||
</abp-card> |
|||
} |
|||
</form> |
|||
</abp-card-body> |
|||
</abp-card> |
|||
``` |
|||
## Step-7 |
|||
|
|||
We implemented token generation infrastructure, now it's time validate the token and let the user in. To do this create a folder named `Controllers` in your ***.Web** project and create a controller, named **PasswordlessController** inside it: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authentication; |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Security.Claims; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace PasswordlessAuthentication.Web.Controllers |
|||
{ |
|||
public class PasswordlessController : AbpController |
|||
{ |
|||
protected IdentityUserManager UserManager { get; } |
|||
|
|||
public PasswordlessController(IdentityUserManager userManager) |
|||
{ |
|||
UserManager = userManager; |
|||
} |
|||
|
|||
public virtual async Task<IActionResult> Login(string token, string userId) |
|||
{ |
|||
var user = await UserManager.FindByIdAsync(userId); |
|||
|
|||
var isValid = await UserManager.VerifyUserTokenAsync(user, "PasswordlessLoginProvider", "passwordless-auth", token); |
|||
if (!isValid) |
|||
{ |
|||
throw new UnauthorizedAccessException("The token " + token + " is not valid for the user " + userId); |
|||
} |
|||
|
|||
await UserManager.UpdateSecurityStampAsync(user); |
|||
|
|||
var roles = await UserManager.GetRolesAsync(user); |
|||
|
|||
var principal = new ClaimsPrincipal( |
|||
new ClaimsIdentity(CreateClaims(user, roles), IdentityConstants.ApplicationScheme) |
|||
); |
|||
|
|||
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, principal); |
|||
|
|||
return Redirect("/"); |
|||
} |
|||
|
|||
private static IEnumerable<Claim> CreateClaims(IUser user, IEnumerable<string> roles) |
|||
{ |
|||
var claims = new List<Claim> |
|||
{ |
|||
new Claim("sub", user.Id.ToString()), |
|||
new Claim(AbpClaimTypes.UserId, user.Id.ToString()), |
|||
new Claim(AbpClaimTypes.Email, user.Email), |
|||
new Claim(AbpClaimTypes.UserName, user.UserName), |
|||
new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString().ToLower()), |
|||
}; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(user.PhoneNumber)) |
|||
{ |
|||
claims.Add(new Claim(AbpClaimTypes.PhoneNumber, user.PhoneNumber)); |
|||
} |
|||
|
|||
foreach (var role in roles) |
|||
{ |
|||
claims.Add(new Claim(AbpClaimTypes.Role, role)); |
|||
} |
|||
|
|||
return claims; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
We created an endpoint for `/Passwordless/Login` that gets the token and the user Id. In this action, we find the user via repository and validate the token via `UserManager.VerifyUserTokenAsync()` method. If it's valid, we create claims of the user then call `HttpContext.SignInAsync` to be able to create an encrypted cookie and add it to the current response. Finally we redirect the page to the root URL. |
|||
|
|||
That's all! We created a passwordless login with 7 steps. |
|||
|
|||
## Source Code |
|||
|
|||
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
|||
@ -0,0 +1,212 @@ |
|||
# How to fix the Chrome login issue for the IdentityServer4 |
|||
|
|||
## Introduction |
|||
|
|||
When you use HTTP on your Identity Server 4 enabled website, users may not login because of the changes made by Chrome in the version 8x. This occurs when you use HTTP schema in your website. The issue is explained here https://docs.microsoft.com/en-gb/dotnet/core/compatibility/3.0-3.1#http-browser-samesite-changes-impact-authentication |
|||
|
|||
## How to solve it? |
|||
|
|||
### Step-1 |
|||
|
|||
Create the below extension in your ***.Web** project. |
|||
|
|||
```csharp |
|||
using System; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Http; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection |
|||
{ |
|||
public static class SameSiteCookiesServiceCollectionExtensions |
|||
{ |
|||
/// <summary> |
|||
/// -1 defines the unspecified value, which tells ASPNET Core to NOT |
|||
/// send the SameSite attribute. With ASPNET Core 3.1 the |
|||
/// <seealso cref="SameSiteMode" /> enum will have a definition for |
|||
/// Unspecified. |
|||
/// </summary> |
|||
private const SameSiteMode Unspecified = (SameSiteMode)(-1); |
|||
|
|||
/// <summary> |
|||
/// Configures a cookie policy to properly set the SameSite attribute |
|||
/// for Browsers that handle unknown values as Strict. Ensure that you |
|||
/// add the <seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" /> |
|||
/// into the pipeline before sending any cookies! |
|||
/// </summary> |
|||
/// <remarks> |
|||
/// Minimum ASPNET Core Version required for this code: |
|||
/// - 2.1.14 |
|||
/// - 2.2.8 |
|||
/// - 3.0.1 |
|||
/// - 3.1.0-preview1 |
|||
/// Starting with version 80 of Chrome (to be released in February 2020) |
|||
/// cookies with NO SameSite attribute are treated as SameSite=Lax. |
|||
/// In order to always get the cookies send they need to be set to |
|||
/// SameSite=None. But since the current standard only defines Lax and |
|||
/// Strict as valid values there are some browsers that treat invalid |
|||
/// values as SameSite=Strict. We therefore need to check the browser |
|||
/// and either send SameSite=None or prevent the sending of SameSite=None. |
|||
/// Relevant links: |
|||
/// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 |
|||
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 |
|||
/// - https://www.chromium.org/updates/same-site |
|||
/// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ |
|||
/// - https://bugs.webkit.org/show_bug.cgi?id=198181 |
|||
/// </remarks> |
|||
/// <param name="services">The service collection to register <see cref="CookiePolicyOptions" /> into.</param> |
|||
/// <returns>The modified <see cref="IServiceCollection" />.</returns> |
|||
public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services) |
|||
{ |
|||
services.Configure<CookiePolicyOptions>(options => |
|||
{ |
|||
options.MinimumSameSitePolicy = Unspecified; |
|||
options.OnAppendCookie = cookieContext => |
|||
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); |
|||
options.OnDeleteCookie = cookieContext => |
|||
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); |
|||
}); |
|||
|
|||
return services; |
|||
} |
|||
|
|||
private static void CheckSameSite(HttpContext httpContext, CookieOptions options) |
|||
{ |
|||
if (options.SameSite == SameSiteMode.None) |
|||
{ |
|||
var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); |
|||
|
|||
if (DisallowsSameSiteNone(userAgent)) |
|||
{ |
|||
options.SameSite = Unspecified; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary> |
|||
/// Checks if the UserAgent is known to interpret an unknown value as Strict. |
|||
/// For those the <see cref="CookieOptions.SameSite" /> property should be |
|||
/// set to <see cref="Unspecified" />. |
|||
/// </summary> |
|||
/// <remarks> |
|||
/// This code is taken from Microsoft: |
|||
/// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ |
|||
/// </remarks> |
|||
/// <param name="userAgent">The user agent string to check.</param> |
|||
/// <returns>Whether the specified user agent (browser) accepts SameSite=None or not.</returns> |
|||
private static bool DisallowsSameSiteNone(string userAgent) |
|||
{ |
|||
// Cover all iOS based browsers here. This includes: |
|||
// - Safari on iOS 12 for iPhone, iPod Touch, iPad |
|||
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad |
|||
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad |
|||
// All of which are broken by SameSite=None, because they use the |
|||
// iOS networking stack. |
|||
// Notes from Thinktecture: |
|||
// Regarding https://caniuse.com/#search=samesite iOS versions lower |
|||
// than 12 are not supporting SameSite at all. Starting with version 13 |
|||
// unknown values are NOT treated as strict anymore. Therefore we only |
|||
// need to check version 12. |
|||
if (userAgent.Contains("CPU iPhone OS 12") |
|||
|| userAgent.Contains("iPad; CPU OS 12")) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
// Cover Mac OS X based browsers that use the Mac OS networking stack. |
|||
// This includes: |
|||
// - Safari on Mac OS X. |
|||
// This does not include: |
|||
// - Chrome on Mac OS X |
|||
// because they do not use the Mac OS networking stack. |
|||
// Notes from Thinktecture: |
|||
// Regarding https://caniuse.com/#search=samesite MacOS X versions lower |
|||
// than 10.14 are not supporting SameSite at all. Starting with version |
|||
// 10.15 unknown values are NOT treated as strict anymore. Therefore we |
|||
// only need to check version 10.14. |
|||
if (userAgent.Contains("Safari") |
|||
&& userAgent.Contains("Macintosh; Intel Mac OS X 10_14") |
|||
&& userAgent.Contains("Version/")) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
// Cover Chrome 50-69, because some versions are broken by SameSite=None |
|||
// and none in this range require it. |
|||
// Note: this covers some pre-Chromium Edge versions, |
|||
// but pre-Chromium Edge does not require SameSite=None. |
|||
// Notes from Thinktecture: |
|||
// We can not validate this assumption, but we trust Microsofts |
|||
// evaluation. And overall not sending a SameSite value equals to the same |
|||
// behavior as SameSite=None for these old versions anyways. |
|||
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
if (GetChromeVersion(userAgent) >= 80) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private static int GetChromeVersion(string userAgent) |
|||
{ |
|||
try |
|||
{ |
|||
return Convert.ToInt32(userAgent.Split("Chrome/")[1].Split('.')[0]); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
return 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Step-2 |
|||
|
|||
Assume that your project name is *Acme.BookStore*. Then open `AcmeBookStoreWebModule.cs` class. |
|||
|
|||
Add the following line to `ConfigureServices()` method. |
|||
|
|||
```csharp |
|||
context.Services.ConfigureNonBreakingSameSiteCookies(); |
|||
``` |
|||
### Step-3 |
|||
|
|||
Go to`OnApplicationInitialization()` method in `AcmeBookStoreWebModule.cs` add `app.UseCookiePolicy();` |
|||
|
|||
```csharp |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var app = context.GetApplicationBuilder(); |
|||
var env = context.GetEnvironment(); |
|||
|
|||
if (env.IsDevelopment()) |
|||
{ |
|||
app.UseDeveloperExceptionPage(); |
|||
} |
|||
else |
|||
{ |
|||
app.UseErrorPage(); |
|||
app.UseHsts(); |
|||
} |
|||
|
|||
app.UseCookiePolicy(); //<--- added this ---> |
|||
|
|||
//.... |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
It's all! You are ready to go! |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
Referenced from https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/ |
|||
@ -0,0 +1,308 @@ |
|||
## Using DevExtreme Components With the ABP Framework |
|||
|
|||
Hi, in this step by step article, I will show you how to integrate [DevExtreme](https://js.devexpress.com/) components into ABP Framework-based applications. |
|||
|
|||
 |
|||
|
|||
*(A screenshot from the example application developed in this article)* |
|||
|
|||
## Create the Project |
|||
|
|||
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\***_ |
|||
|
|||
## 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 into your computer. |
|||
|
|||
> Don't forget to add _"DevExpress NuGet Feed"_ to your **Nuget Package Sources**. |
|||
|
|||
### 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 |
|||
``` |
|||
|
|||
### Adding DevExtreme NPM Dependencies |
|||
|
|||
Open your `DevExtremeSample.Web` project folder with a command line and add `devextreme` and `devextreme-aspnet-data` NPM packages: |
|||
|
|||
````bash |
|||
npm install devextreme |
|||
```` |
|||
|
|||
````bash |
|||
npm install devextreme-aspnet-data |
|||
```` |
|||
|
|||
### Adding Resource Mappings |
|||
|
|||
The `devextreme` and `devextreme-aspnet-data` NPM packages are 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 definitions to inside `mappings` object. |
|||
|
|||
````json |
|||
"@node_modules/devextreme/dist/**/*": "@libs/devextreme/", |
|||
"@node_modules/devextreme-aspnet-data/js/dx.aspnet.data.js": "@libs/devextreme/js/" |
|||
```` |
|||
|
|||
The final `abp.resourcemapping.js` file should look like below: |
|||
|
|||
``` |
|||
module.exports = { |
|||
aliases: {}, |
|||
mappings: { |
|||
"@node_modules/devextreme/dist/**/*": "@libs/devextreme/", |
|||
"@node_modules/devextreme-aspnet-data/js/dx.aspnet.data.js": "@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 can not add DevExtreme js packages to Global Script Bundles, just like done for the CSS files. Because DevExtreme requires to add its JavaScript files into the `<head>` section of the HTML document, while ABP Framework adds all JavaScript files to the end of the `<body>` (as a best practice). |
|||
|
|||
Fortunately, ABP Framework has a [layout hook system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface#layout-hooks) that allows you to add any code into some specific positions in the HTML document. All you need to do is to create a `ViewComponent` and configure the layout hooks. |
|||
|
|||
Let's begin by creating a `DevExtremeScriptContributor.cs` file in the `Bundling` folder by copying the following code 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` depends on `JQueryScriptContributor` which adds JQuery related files before the DevExpress packages (see the [bundling system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification) for details). |
|||
|
|||
#### Create DevExtremeJsViewComponent |
|||
|
|||
Create a new view component, named `DevExtremeJsViewComponent` inside the `/Components/DevExtremeJs` folder of the Web project, by following the steps below: |
|||
|
|||
1) Create a `DevExtremeJsViewComponent` class inside the `/Components/DevExtremeJs` (create the folders first): |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace DevExtremeSample.Web.Components.DevExtremeJs |
|||
{ |
|||
public class DevExtremeJsViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View("/Components/DevExtremeJs/Default.cshtml"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
2) Create `Default.cshtml` file in the same folder with the following content: |
|||
|
|||
```csharp |
|||
@using DevExtremeSample.Web.Bundling |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
|
|||
<abp-script type="typeof(DevExtremeScriptContributor)" /> |
|||
``` |
|||
|
|||
Your final Web project should be like the following: |
|||
|
|||
 |
|||
|
|||
3) Now, we can add this view component to `<head>` section by using the layout hooks. |
|||
|
|||
Open your `DevExtremeSampleWebModule.cs` file in your `DevExtremeSample.Web` project and add following code into the `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. |
|||
|
|||
> This issue does exist in the ABP Framework v3.0 and earlier versions. If you are using ABP Framework v3.1 or a later version, 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. |
|||
|
|||
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 the `wwwroot/libs/abp/jquery/abp.jquery.js` file of your Web project. |
|||
|
|||
### Result |
|||
|
|||
The installation step was done. You can use any DevExtreme component in your application. |
|||
|
|||
Example: A button and a progress bar component: |
|||
|
|||
 |
|||
|
|||
This example has been created by following [this documentation](https://js.devexpress.com/Demos/WidgetsGallery/Demo/ProgressBar/Overview/NetCore/Light/). |
|||
|
|||
## The 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. |
|||
|
|||
### The Source Code |
|||
|
|||
You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Mvc). |
|||
|
|||
### Data Grid |
|||
|
|||
You can see the full working example of [Data Grid](https://demos.devexpress.com/ASPNetCore/Demo/DataGrid/Overview/). |
|||
|
|||
 |
|||
|
|||
The related files for this example are highlighted at the following screenshots. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
### Tree List |
|||
|
|||
|
|||
You can see the full working example of [Tree List](https://demos.devexpress.com/ASPNetCore/Demo/TreeList/Overview/). |
|||
|
|||
 |
|||
|
|||
The related files for this example are highlighted at the following screenshots. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
### Additional Notes |
|||
|
|||
#### Data Storage |
|||
|
|||
I've used an in-memory list to store data for this example, instead of a real database. Because it is not related to DevExpress usage. There is a `SampleDataService.cs` file in `Data` folder at `.Application.Contracts` project. All the data is stored here. |
|||
|
|||
#### JSON Serialization |
|||
|
|||
You can see some `JsonProperty` attributes on the DTO properties. I use these attributes because DevExtreme example expects `PascalCase` property names in the serialized JSON that is sent to the client. But ABP Framework & ASP.NET Core conventionally uses `camelCase` property names on JSON serialization. Adding these `JsonProperty` attributes ensures that the related properties are serialized as `PascalCase`. |
|||
|
|||
#### DevExtreme Components vs Application Service Methods |
|||
|
|||
ABP Framework conventionally converts application services to API Controllers. For example, see the application service below: |
|||
|
|||
````csharp |
|||
public class OrderAppService : DevExtremeSampleAppService, IOrderAppService |
|||
{ |
|||
public async Task<LoadResult> GetOrdersAsync(DataSourceLoadOptions loadOptions) |
|||
{ |
|||
... |
|||
} |
|||
|
|||
public async Task<Order> InsertOrder(string values) |
|||
{ |
|||
... |
|||
} |
|||
... |
|||
} |
|||
```` |
|||
|
|||
You can use these service methods for your DevExtreme components as shown below: |
|||
|
|||
```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") |
|||
) |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I've explained how to use [DevExtreme](https://js.devexpress.com/) components in your application. ABP Framework is designed so that it can work with any UI library/framework. |
|||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
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: 11 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,81 @@ |
|||
# ABP Suite: How to Add the User Entity as a Navigation Property of Another Entity |
|||
|
|||
## Introduction |
|||
|
|||
[ABP Suite](https://commercial.abp.io/tools/suite), a part of the [ABP Commercial](https://commercial.abp.io/), is a productivity tool developed by the team behind the ABP Framework. The main functionality of the ABP Suite is to generate code for you. |
|||
|
|||
In this post, I'll show you how to add the user entity as a navigation property in your new entity, by the help of the ABP Suite. |
|||
|
|||
> In the sample project MVC UI is used, but the same steps are applicable to the Angular UI as well. |
|||
|
|||
## Code Generation |
|||
|
|||
### Create a New Entity |
|||
|
|||
Open the ABP Suite ([see how](https://docs.abp.io/en/commercial/latest/abp-suite/index)). Create a new entity called `Note`, as an example entity. |
|||
|
|||
 |
|||
|
|||
Then add a string property called `Title`, as an example property. |
|||
|
|||
 |
|||
|
|||
### Create AppUserDto |
|||
|
|||
ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property. |
|||
|
|||
To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`. |
|||
|
|||
 |
|||
|
|||
We should define the [object mapping](https://docs.abp.io/en/abp/latest/Object-To-Object-Mapping) to be able to convert the `AppUser` objects to `AppUserDto` objects. To do this, open `YourProjectApplicationAutoMapperProfile.cs` and add the below line: |
|||
|
|||
```csharp |
|||
CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties); |
|||
``` |
|||
|
|||
 |
|||
|
|||
> Creating such a DTO class may not be needed for another entity than the `AppUser`, since it will probably be already available, especially if you had created the other entity using the ABP Suite. |
|||
|
|||
### Define the Navigation Property |
|||
|
|||
Get back to ABP Suite, open the **Navigation Properties** tab of the ABP Suite, click the **Add Navigation Property** button. Browse `AppUser.cs` in `*.Domain\Users` folder. Then choose the `Name` item as display property. Browse `AppUserDto.cs` in `*.Contracts\Users` folder. Choose `Users` from Collection Names dropdown. |
|||
|
|||
 |
|||
|
|||
### Generate the Code! |
|||
|
|||
That's it! Click **Save and generate** button to create your page. You'll see the following page if everything goes well. |
|||
|
|||
 |
|||
|
|||
This is the new page that has been created by the ABP Suite. It can perform the fundamental CRUD operations. Also, it has the "App user" column that shows the related user name (you can easily change the automatically created "App user" title from the **Entity Name** field of the navigation property creation screen). |
|||
|
|||
**Picking Users from Look Up Table** |
|||
|
|||
We used dropdown element to select a user from the user list. If you have a lot of users, then it's good to pick a user from a look up table. A look up table is a modal window that lets you filter data and pick one. To do this, get back to Suite and click **Edit** button of user navigation which is set as `AppUserId` name. Choose "Modal" from the "UI Pick Type" field. Then click **Save and generate** button to recreate your page. |
|||
|
|||
 |
|||
|
|||
After successful code generation, you'll see the the user can be picked from user table. |
|||
|
|||
 |
|||
|
|||
## About the ABP Commercial RC |
|||
|
|||
This example has been implemented with **ABP Commercial 3.1.0**. If you have not installed the ABP CLI and ABP Suite, follow the next steps: |
|||
|
|||
1- Uninstall the current version of the CLI and install: |
|||
|
|||
```bash |
|||
dotnet tool install --global Volo.Abp.Cli --version 3.1.0 |
|||
``` |
|||
|
|||
2- Uninstall the current version of the Suite and install: |
|||
|
|||
```bash |
|||
dotnet tool uninstall --global Volo.Abp.Suite && dotnet tool install -g Volo.Abp.Suite --version 3.1.0 --add-source https://nuget.abp.io/<YOUR-API-KEY>/v3/index.json |
|||
``` |
|||
|
|||
Don't forget to replace the `<YOUR-API-KEY>` with your own key! |
|||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 539 KiB |
|
After Width: | Height: | Size: 175 KiB |
@ -0,0 +1,167 @@ |
|||
# Distributed Event Bus Kafka Integration |
|||
|
|||
> This document explains **how to configure the [Kafka](https://kafka.apache.org/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Kafka` package. |
|||
* Run `abp add-package Volo.Abp.EventBus.Kafka` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusKafkaModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. |
|||
|
|||
### `appsettings.json` file configuration |
|||
|
|||
This is the simplest way to configure the Kafka settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). |
|||
|
|||
**Example: The minimal configuration to connect to a local kafka server with default configurations** |
|||
|
|||
````json |
|||
{ |
|||
"Kafka": { |
|||
"Connections": { |
|||
"Default": { |
|||
"BootstrapServers": "localhost:9092" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"GroupId": "MyGroupId", |
|||
"TopicName": "MyTopicName" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `MyGroupId` is the name of this application, which is used as the **GroupId** on the Kakfa. |
|||
* `MyTopicName` is the **topic name**. |
|||
|
|||
See [the Kafka document](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html) to understand these options better. |
|||
|
|||
#### Connections |
|||
|
|||
If you need to connect to another server than the localhost, you need to configure the connection properties. |
|||
|
|||
**Example: Specify the host name (as an IP address)** |
|||
|
|||
````json |
|||
{ |
|||
"Kafka": { |
|||
"Connections": { |
|||
"Default": { |
|||
"BootstrapServers": "123.123.123.123:9092" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"GroupId": "MyGroupId", |
|||
"TopicName": "MyTopicName" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus. |
|||
|
|||
**Example: Declare two connections and use one of them for the event bus** |
|||
|
|||
````json |
|||
{ |
|||
"Kafka": { |
|||
"Connections": { |
|||
"Default": { |
|||
"BootstrapServers": "123.123.123.123:9092" |
|||
}, |
|||
"SecondConnection": { |
|||
"BootstrapServers": "321.321.321.321:9092" |
|||
} |
|||
}, |
|||
"EventBus": { |
|||
"GroupId": "MyGroupId", |
|||
"TopicName": "MyTopicName", |
|||
"ConnectionName": "SecondConnection" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus. |
|||
|
|||
You can use any of the [ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html) properties as the connection properties. |
|||
|
|||
**Example: Specify the socket timeout** |
|||
|
|||
````json |
|||
{ |
|||
"Kafka": { |
|||
"Connections": { |
|||
"Default": { |
|||
"BootstrapServers": "123.123.123.123:9092", |
|||
"SocketTimeoutMs": 60000 |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### The Options Classes |
|||
|
|||
`AbpRabbitMqOptions` and `AbpRabbitMqEventBusOptions` classes can be used to configure the connection strings and event bus options for the RabbitMQ. |
|||
|
|||
You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). |
|||
|
|||
**Example: Configure the connection** |
|||
|
|||
````csharp |
|||
Configure<AbpKafkaOptions>(options => |
|||
{ |
|||
options.Connections.Default.BootstrapServers = "123.123.123.123:9092"; |
|||
options.Connections.Default.SaslUsername = "user"; |
|||
options.Connections.Default.SaslPassword = "pwd"; |
|||
}); |
|||
```` |
|||
|
|||
**Example: Configure the consumer config** |
|||
|
|||
````csharp |
|||
Configure<AbpKafkaOptions>(options => |
|||
{ |
|||
options.ConfigureConsumer = config => |
|||
{ |
|||
config.GroupId = "MyGroupId"; |
|||
config.EnableAutoCommit = false; |
|||
}; |
|||
}); |
|||
```` |
|||
|
|||
**Example: Configure the producer config** |
|||
|
|||
````csharp |
|||
Configure<AbpKafkaOptions>(options => |
|||
{ |
|||
options.ConfigureProducer = config => |
|||
{ |
|||
config.MessageTimeoutMs = 6000; |
|||
config.Acks = Acks.All; |
|||
}; |
|||
}); |
|||
```` |
|||
|
|||
**Example: Configure the topic specification** |
|||
|
|||
````csharp |
|||
Configure<AbpKafkaOptions>(options => |
|||
{ |
|||
options.ConfigureTopic = specification => |
|||
{ |
|||
specification.ReplicationFactor = 3; |
|||
specification.NumPartitions = 3; |
|||
}; |
|||
}); |
|||
```` |
|||
|
|||
Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. |
|||
@ -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) |
|||
@ -1,3 +1,448 @@ |
|||
# Features |
|||
|
|||
TODO |
|||
ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**. |
|||
|
|||
The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature. |
|||
|
|||
Feature system was originally designed to control the tenant features in a **[multi-tenant](Multi-Tenancy.md)** application. However, it is **extensible** and capable of determining the features by any condition. |
|||
|
|||
> 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<PdfReportResultDto> 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<T>`) are intercepted. |
|||
|
|||
> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP Framework uses the action/page filters to implement the feature checking in this case. |
|||
|
|||
### IFeatureChecker Service |
|||
|
|||
`IFeatureChecker` allows to check a feature in your application code. |
|||
|
|||
#### IsEnabledAsync |
|||
|
|||
Returns `true` if the given feature is enabled. So, you can conditionally execute your business flow. |
|||
|
|||
**Example: Check if the "PDF Reporting" feature enabled** |
|||
|
|||
```csharp |
|||
public class ReportingAppService : ApplicationService, IReportingAppService |
|||
{ |
|||
private readonly IFeatureChecker _featureChecker; |
|||
|
|||
public ReportingAppService(IFeatureChecker featureChecker) |
|||
{ |
|||
_featureChecker = featureChecker; |
|||
} |
|||
|
|||
public async Task<PdfReportResultDto> GetPdfReportAsync() |
|||
{ |
|||
if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) |
|||
{ |
|||
//TODO... |
|||
} |
|||
else |
|||
{ |
|||
//TODO... |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`IsEnabledAsync` has overloads to check multiple features in one method call. |
|||
|
|||
#### GetOrNullAsync |
|||
|
|||
Gets the current value for a feature. This method returns a `string`, so you store any kind of value inside it, by converting to or from `string`. |
|||
|
|||
**Example: Check the maximum product count allowed** |
|||
|
|||
```csharp |
|||
public class ProductController : AbpController |
|||
{ |
|||
private readonly IFeatureChecker _featureChecker; |
|||
|
|||
public ProductController(IFeatureChecker featureChecker) |
|||
{ |
|||
_featureChecker = featureChecker; |
|||
} |
|||
|
|||
public async Task<IActionResult> Create(CreateProductModel model) |
|||
{ |
|||
var currentProductCount = await GetCurrentProductCountFromDatabase(); |
|||
|
|||
//GET THE FEATURE VALUE |
|||
var maxProductCountLimit = |
|||
await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount"); |
|||
|
|||
if (currentProductCount >= Convert.ToInt32(maxProductCountLimit)) |
|||
{ |
|||
throw new BusinessException( |
|||
"MyApp:ReachToMaxProductCountLimit", |
|||
$"You can not create more than {maxProductCountLimit} products!" |
|||
); |
|||
} |
|||
|
|||
//TODO: Create the product in the database... |
|||
} |
|||
|
|||
private async Task<int> GetCurrentProductCountFromDatabase() |
|||
{ |
|||
throw new System.NotImplementedException(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application. |
|||
|
|||
Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method: |
|||
|
|||
```csharp |
|||
var maxProductCountLimit = await _featureChecker.GetAsync<int>("MyApp.MaxProductCount"); |
|||
``` |
|||
|
|||
#### Extension Methods |
|||
|
|||
There are some useful extension methods for the `IFeatureChecker` interface; |
|||
|
|||
* `Task<T> GetAsync<T>(string name, T defaultValue = default)`: Used to get a value of a feature with the given type `T`. Allows to specify a `defaultValue` that is returned when the feature value is `null`. |
|||
* `CheckEnabledAsync(string name)`: Checks if given feature is enabled. Throws an `AbpAuthorizationException` if the feature was not `true` (enabled). |
|||
|
|||
## Defining the Features |
|||
|
|||
A feature should be defined to be able to check it. |
|||
|
|||
### FeatureDefinitionProvider |
|||
|
|||
Create a class inheriting the `FeatureDefinitionProvider` to define permissions. |
|||
|
|||
**Example: Defining permissions** |
|||
|
|||
```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<FeaturesDemoResource>("PdfReporting"), |
|||
valueType: new ToggleStringValueType() |
|||
); |
|||
|
|||
myGroup.AddFeature( |
|||
"MyApp.MaxProductCount", |
|||
defaultValue: "10", |
|||
displayName: LocalizableString |
|||
.Create<FeaturesDemoResource>("MaxProductCount"), |
|||
valueType: new FreeTextStringValueType( |
|||
new NumericValueValidator(0, 1000000)) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
* `FeaturesDemoResource` is the project name in this example code. See the [localization document](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): |
|||
|
|||
 |
|||
|
|||
This action opens a modal to manage the feature values for the selected tenant: |
|||
|
|||
 |
|||
|
|||
So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application. |
|||
|
|||
See the *Feature Management* section below to learn more about managing the features. |
|||
|
|||
### Child Features |
|||
|
|||
A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled. |
|||
|
|||
**Example: Defining child features** |
|||
|
|||
```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<FeaturesDemoResource>("Reporting"), |
|||
valueType: new ToggleStringValueType() |
|||
); |
|||
|
|||
reportingFeature.CreateChild( |
|||
"MyApp.PdfReporting", |
|||
defaultValue: "false", |
|||
displayName: LocalizableString |
|||
.Create<FeaturesDemoResource>("PdfReporting"), |
|||
valueType: new ToggleStringValueType() |
|||
); |
|||
|
|||
reportingFeature.CreateChild( |
|||
"MyApp.ExcelReporting", |
|||
defaultValue: "false", |
|||
displayName: LocalizableString |
|||
.Create<FeaturesDemoResource>("ExcelReporting"), |
|||
valueType: new ToggleStringValueType() |
|||
); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The example above defines a *Reporting* feature with two children: *PDF Reporting* and *Excel Reporting*. |
|||
|
|||
### Changing Features Definitions of a Depended Module |
|||
|
|||
A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing permission definitions (defined by the depended [modules](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: |
|||
|
|||
 |
|||
|
|||
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<string> GetOrNullAsync(FeatureDefinition feature) |
|||
{ |
|||
if (feature.ValueType is ToggleStringValueType && |
|||
_currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") |
|||
{ |
|||
return Task.FromResult("true"); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
If a provider returns `null`, then the next provider is executed. |
|||
|
|||
Once a provider is defined, it should be added to the `AbpFeatureOptions` as shown below: |
|||
|
|||
```csharp |
|||
Configure<AbpFeatureOptions>(options => |
|||
{ |
|||
options.ValueProviders.Add<SystemAdminFeatureValueProvider>(); |
|||
}); |
|||
``` |
|||
|
|||
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 |
|||
@ -1,181 +0,0 @@ |
|||
# Getting Started ABP With Console Application |
|||
|
|||
This tutorial explains how to start ABP from scratch with minimal dependencies. You generally want to start with a **[startup template](https://abp.io/Templates)**. |
|||
|
|||
## Create A New Project |
|||
|
|||
Create a new Regular .Net Core Console Application from Visual Studio: |
|||
|
|||
 |
|||
|
|||
## Install Volo.Abp Package |
|||
|
|||
Volo.Abp.Core is the core nuget package to create ABP based applications. So, install it to your project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Core |
|||
```` |
|||
|
|||
## Create First ABP Module |
|||
|
|||
ABP is a modular framework and it requires a **startup (root) module** class derived from ``AbpModule``: |
|||
|
|||
````C# |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
``AppModule`` is a good name for the startup module for an application. |
|||
|
|||
## Initialize The Application |
|||
|
|||
The next step is to bootstrap the application using the startup module created above: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``AbpApplicationFactory`` is used to create the application and load all modules taking ``AppModule`` as the startup module. ``Initialize()`` method starts the application. |
|||
|
|||
## Hello World! |
|||
|
|||
The application above does nothing. Let's create a service that does something: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class HelloWorldService : ITransientDependency |
|||
{ |
|||
public void SayHello() |
|||
{ |
|||
Console.WriteLine("Hello World!"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``ITransientDependency`` is a special interface of ABP that automatically registers the service as transient (see [dependency injection document](Dependency-Injection.md)). |
|||
|
|||
Now, we can resolve the ``HelloWorldService`` and say hello. Change the Program.cs as shown below: |
|||
|
|||
````C# |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>()) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
//Resolve a service and use it |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
While it's enough for this simple code example, it's always suggested to create scopes in case of directly resolving dependencies from ``IServiceProvider`` (see the [Dependency Injection documentation](Dependency-Injection.md)). |
|||
|
|||
## Using Autofac as the Dependency Injection Framework |
|||
|
|||
While AspNet Core's Dependency Injection (DI) system is fine for basic requirements, Autofac provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features. |
|||
|
|||
Replacing AspNet Core's DI system by Autofac and integrating to ABP is pretty easy. |
|||
|
|||
1. Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) package |
|||
|
|||
``` |
|||
Install-Package Volo.Abp.Autofac |
|||
``` |
|||
|
|||
1. Add ``AbpAutofacModule`` Dependency |
|||
|
|||
```c# |
|||
[DependsOn(typeof(AbpAutofacModule))] //Add dependency to the AbpAutofacModule |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
``` |
|||
|
|||
1. Change ``Program.cs`` file as shown below: |
|||
|
|||
```c# |
|||
using System; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
class Program |
|||
{ |
|||
static void Main(string[] args) |
|||
{ |
|||
using (var application = AbpApplicationFactory.Create<AppModule>(options => |
|||
{ |
|||
options.UseAutofac(); //Autofac integration |
|||
})) |
|||
{ |
|||
application.Initialize(); |
|||
|
|||
//Resolve a service and use it |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Just called `options.UseAutofac()` method in the `AbpApplicationFactory.Create` options. |
|||
|
|||
## Source Code |
|||
|
|||
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication). |
|||
@ -0,0 +1,3 @@ |
|||
# Global Features |
|||
|
|||
TODO (see [#5061](https://github.com/abpframework/abp/issues/5061) until this is documented). |
|||
@ -1,9 +0,0 @@ |
|||
# "How To" Guides |
|||
|
|||
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. |
|||
|
|||
## Authentication |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) |
|||
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) |
|||
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) |
|||
@ -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). |
|||
@ -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 |
|||
@ -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 |
|||
@ -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. |
|||