@ -0,0 +1,38 @@ |
|||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app |
|||
|
|||
# Number of days of inactivity before a closed issue or pull request is locked |
|||
daysUntilLock: 30 |
|||
|
|||
# Skip issues and pull requests created before a given timestamp. Timestamp must |
|||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable |
|||
skipCreatedBefore: false |
|||
|
|||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable |
|||
exemptLabels: [] |
|||
|
|||
# Label to add before locking, such as `outdated`. Set to `false` to disable |
|||
lockLabel: false |
|||
|
|||
# Comment to post before locking. Set to `false` to disable |
|||
lockComment: > |
|||
This thread has been automatically locked since there has not been |
|||
any recent activity after it was closed. Please open a new issue for |
|||
related bugs. |
|||
|
|||
# Assign `resolved` as the reason for locking. Set to `false` to disable |
|||
setLockReason: true |
|||
|
|||
# Limit to only `issues` or `pulls` |
|||
# only: issues |
|||
|
|||
# Optionally, specify configuration settings just for `issues` or `pulls` |
|||
# issues: |
|||
# exemptLabels: |
|||
# - help-wanted |
|||
# lockLabel: outdated |
|||
|
|||
# pulls: |
|||
# daysUntilLock: 30 |
|||
|
|||
# Repository to extend settings from |
|||
# _extends: repo |
|||
@ -0,0 +1,15 @@ |
|||
# Number of days of inactivity before an issue becomes stale |
|||
daysUntilStale: 60 |
|||
# Number of days of inactivity before a stale issue is closed |
|||
daysUntilClose: 7 |
|||
# Set to true to ignore issues in a milestone (defaults to false) |
|||
exemptMilestones: true |
|||
# Label to use when marking an issue as stale |
|||
staleLabel: inactive |
|||
# Comment to post when marking an issue as stale. Set to `false` to disable |
|||
markComment: > |
|||
This issue has been automatically marked as stale because it has not had |
|||
recent activity. It will be closed if no further activity occurs. Thank you |
|||
for your contributions. |
|||
# Comment to post when closing a stale issue. Set to `false` to disable |
|||
closeComment: false |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"culture": "ar", |
|||
"texts": { |
|||
"Account": "حساب ABP - تسجيل الدخول وإنشاء حساب | ABP.IO", |
|||
"Welcome": "أهلا و سهلا", |
|||
"UseOneOfTheFollowingLinksToContinue": "استخدم أحد الروابط التالية للمتابعة", |
|||
"FrameworkHomePage": "الصفحة الرئيسية للنظام", |
|||
"FrameworkDocumentation": "وثائق النظام", |
|||
"OfficialBlog": "المدونة الرسمية", |
|||
"CommercialHomePage": "الصفحة الرئيسية للنظام التجاري", |
|||
"CommercialSupportWebSite": "موقع الدعم الفني للنظام التجاري", |
|||
"CommunityWebSite": "موقع مجتمع ABP" |
|||
} |
|||
} |
|||
@ -0,0 +1,161 @@ |
|||
{ |
|||
"culture": "ar", |
|||
"texts": { |
|||
"Permission:Organizations": "المنظمات", |
|||
"Permission:Manage": "إدارة المنظمات", |
|||
"Permission:DiscountRequests": "طلبات الخصم", |
|||
"Permission:DiscountManage": "إدارة طلبات الخصم", |
|||
"Permission:Disable": "تعطيل", |
|||
"Permission:Enable": "تفعيل", |
|||
"Permission:EnableSendEmail": "تمكين إرسال البريد الإلكتروني", |
|||
"Permission:SendEmail": "ارسل بريد الكتروني", |
|||
"Permission:NpmPackages": "حزم NPM", |
|||
"Permission:NugetPackages": "حزم Nuget", |
|||
"Permission:Maintenance": "صيانة", |
|||
"Permission:Maintain": "المحافظة", |
|||
"Permission:ClearCaches": "مسح ذاكرة التخزين المؤقت", |
|||
"Permission:Modules": "الوحدات", |
|||
"Permission:Packages": "حزم", |
|||
"Permission:Edit": "تعديل", |
|||
"Permission:Delete": "حذف", |
|||
"Permission:Create": "إنشاء", |
|||
"Permission:Accounting": "محاسبة", |
|||
"Permission:Accounting:Quotation": "عرض سعر", |
|||
"Permission:Accounting:Invoice": "فاتورة", |
|||
"Menu:Organizations": "المنظمات", |
|||
"Menu:Accounting": "المحاسبة", |
|||
"Menu:Packages": "الحزم", |
|||
"Menu:DiscountRequests": "طلبات الخصم", |
|||
"NpmPackageDeletionWarningMessage": "سيتم حذف حزمة NPM هذه. هل تؤكد ذلك؟", |
|||
"NugetPackageDeletionWarningMessage": "سيتم حذف حزمة Nuget هذه. هل تؤكد ذلك؟", |
|||
"ModuleDeletionWarningMessage": "سيتم حذف هذه الوحدة. هل تؤكد ذلك؟", |
|||
"Name": "اسم", |
|||
"DisplayName": "اسم العرض", |
|||
"ShortDescription": "وصف قصير", |
|||
"NameFilter": "الاسم", |
|||
"CreationTime": "وقت الإنشاء", |
|||
"IsPro": "هو pro", |
|||
"ShowOnModuleList": "إظهار في قائمة الوحدات", |
|||
"EfCoreConfigureMethodName": "تكوين اسم method", |
|||
"IsProFilter": "هو pro", |
|||
"ApplicationType": "نوع التطبيق", |
|||
"Target": "استهداف", |
|||
"TargetFilter": "استهداف", |
|||
"Edit": "تعديل", |
|||
"Delete": "حذف", |
|||
"Refresh": "تحديث", |
|||
"NpmPackages": "حزم NPM", |
|||
"NugetPackages": "حزم Nuget", |
|||
"NpmPackageCount": "عدد حزم NPM", |
|||
"NugetPackageCount": "عدد حزم Nuget", |
|||
"Module": "الوحدات", |
|||
"ModuleInfo": "معلومات الوحدة", |
|||
"CreateANpmPackage": "إنشاء حزمة NPM", |
|||
"CreateAModule": "إنشاء وحدة", |
|||
"CreateANugetPackage": "أنشاء حزمة Nuget", |
|||
"AddNew": "اضف جديد", |
|||
"PackageAlreadyExist{0}": "", |
|||
"ModuleAlreadyExist{0}": "", |
|||
"ClearCache": "مسح ذاكرة التخزين المؤقت", |
|||
"SuccessfullyCleared": "تم محوه بنجاح", |
|||
"Menu:NpmPackages": "حزم NPM", |
|||
"Menu:Modules": "الوحدات", |
|||
"Menu:Maintenance": "الصيانة", |
|||
"Menu:NugetPackages": "حزم Nuget", |
|||
"CreateAnOrganization": "إنشاء منظمة", |
|||
"Organizations": "المنظمات", |
|||
"LongName": "اسم طويل", |
|||
"LicenseType": "نوع الرخصة", |
|||
"MissingLicenseTypeField": "حقل نوع الرخصة مطلوب!", |
|||
"LicenseStartTime": "وقت بدء الرخصة", |
|||
"LicenseEndTime": "وقت إنتهاء الرخصة", |
|||
"AllowedDeveloperCount": "عدد مطوري البرامج المسموح به", |
|||
"UserNameOrEmailAddress": "اسم المستخدم أو البريد الالكتروني", |
|||
"AddOwner": "أضف مالك", |
|||
"UserName": "اسم المستخدم", |
|||
"Email": "البريد الإلكتروني", |
|||
"Developers": "المطورين", |
|||
"AddDeveloper": "أضف مطور", |
|||
"Create": "إنشاء", |
|||
"UserNotFound": "لم نعثر على المستخدم", |
|||
"{0}WillBeRemovedFromDevelopers": "{0} ستتم إزالته من المطورين ، هل تؤكد ذلك؟", |
|||
"{0}WillBeRemovedFromOwners": "{0} ستتم إزالته من المالكين ، هل تؤكد ذلك؟", |
|||
"Computers": "أجهزة الكمبيوتر", |
|||
"UniqueComputerId": "معرف الكمبيوتر", |
|||
"LastSeenDate": "تاريخ آخر ظهور", |
|||
"{0}Computer{1}WillBeRemovedFromRecords": "ستتم إزالة كمبيوتر {0} ({1}) من السجلات", |
|||
"OrganizationDeletionWarningMessage": "سيتم حذف المنظمة", |
|||
"DeletingLastOwnerWarningMessage": "يجب أن يكون للمنظمة مالك واحد على الأقل! لذلك لا يمكنك إزالة هذا المالك", |
|||
"This{0}AlreadyExistInThisOrganization": "هذا {0} موجود بالفعل في هذه المنظمة", |
|||
"AreYouSureYouWantToDeleteAllComputers": "هل أنت متأكد أنك تريد حذف جميع أجهزة الكمبيوتر؟", |
|||
"DeleteAll": "حذف الكل", |
|||
"DoYouWantToCreateNewUser": "هل تريد إنشاء مستخدم جديد؟", |
|||
"MasterModules": "الوحدات الرئيسية", |
|||
"OrganizationName": "اسم المنظمة", |
|||
"CreationDate": "تاريخ الإنشاء", |
|||
"LicenseStart": "تاريخ بدء الرخصة", |
|||
"LicenseEndDate": "تاريخ إنتهاء الرخصة", |
|||
"OrganizationNamePlaceholder": "اسم المنظمة ...", |
|||
"TotalQuestionCountPlaceholder": "إجمالي عدد الأسئلة ...", |
|||
"RemainingQuestionCountPlaceholder": "عدد الأسئلة المتبقية ...", |
|||
"LicenseTypePlaceholder": "نوع الرخصة...", |
|||
"CreationDatePlaceholder": "تاريخ الإنشاء...", |
|||
"LicenseStartDatePlaceholder": "تاريخ بدء الرخصة ...", |
|||
"LicenseEndDatePlaceholder": "تاريخ إنتهاء الرخصة ...", |
|||
"UsernameOrEmail": "اسم المستخدم أو البريد الالكتروني", |
|||
"UsernameOrEmailPlaceholder": "اسم المستخدم أو البريد الالكتروني...", |
|||
"Member": "عضو", |
|||
"PurchaseOrderNo": "طلب الشراء رقم", |
|||
"QuotationDate": "تاريخ عرض السعر", |
|||
"CompanyName": "اسم الشركة", |
|||
"CompanyAddress": "عنوان الشركة", |
|||
"Price": "السعر", |
|||
"DiscountText": "نص الخصم", |
|||
"DiscountQuantity": "كمية الخصم", |
|||
"DiscountPrice": "سعر الخصم", |
|||
"Quotation": "عرض السعر", |
|||
"ExtraText": "نص إضافي", |
|||
"ExtraAmount": "مبلغ إضافي", |
|||
"DownloadQuotation": "تنزيل عرض الأسعار", |
|||
"Invoice": "فاتورة", |
|||
"TaxNumber": "الرقم الضريبي", |
|||
"InvoiceNumber": "رقم الفاتورة", |
|||
"InvoiceDate": "تاريخ الفاتورة", |
|||
"InvoiceNote": "ملاحظة الفاتورة", |
|||
"Quantity": "كمية", |
|||
"AddProduct": "أضف منتج", |
|||
"AddProductWarning": "تحتاج إلى إضافة منتج!", |
|||
"TotalPrice": "السعر الكلي", |
|||
"Generate": "انشاء", |
|||
"MissingQuantityField": "حقل الكمية مطلوب!", |
|||
"MissingPriceField": "حقل السعر مطلوب!", |
|||
"CodeUsageStatus": "الحالة", |
|||
"Country": "الدولة", |
|||
"DeveloperCount": "عدد المطورين", |
|||
"RequestCode": "كود الطلب", |
|||
"WebSite": "موقع الكتروني", |
|||
"GithubUsername": "اسم مستخدم Github", |
|||
"PhoneNumber": "رقم الهاتف", |
|||
"ProjectDescription": "وصف المشروع", |
|||
"Referrer": "المُحيل", |
|||
"DiscountRequests": "طلب الخصم", |
|||
"Copylink": "انسخ الرابط", |
|||
"Disable": "تعطيل", |
|||
"Enable": "تفعيل", |
|||
"EnableSendEmail": "تمكين إرسال البريد الإلكتروني", |
|||
"SendEmail": "ارسل بريد الكتروني", |
|||
"SuccessfullyDisabled": "تم التعطيل بنجاح", |
|||
"SuccessfullyEnabled": "تم التفعيل بنجاح", |
|||
"EmailSent": "تم ارسال البريد الالكتروني", |
|||
"SuccessfullySent": "أرسلت بنجاح", |
|||
"SuccessfullyDeleted": "تم الحذف بنجاح", |
|||
"DiscountRequestDeletionWarningMessage": "سيتم حذف طلب الخصم", |
|||
"BusinessType": "نوع العمل", |
|||
"TotalQuestionCount": "إجمالي عدد الأسئلة", |
|||
"RemainingQuestionCount": "عدد الأسئلة المتبقية", |
|||
"TotalQuestionMustBeGreaterWarningMessage": "يجب أن يكون TotalQuestionCount أكبر من RemainingQuestionCount!", |
|||
"QuestionCountsMustBeGreaterThanZero": "يجب أن يكون TotalQuestionCount و RemainingQuestionCount صفرًا أو أكبر من الصفر!", |
|||
"UnlimitedQuestionCount": "عدد أسئلة غير محدود", |
|||
"Notes": "ملاحظات" |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace AbpIoLocalization.Community.Localization |
|||
{ |
|||
[LocalizationResourceName("AbpIoCommunity")] |
|||
public class AbpIoCommunityResource |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
{ |
|||
"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", |
|||
"LatestArticles": "Latest Articles", |
|||
"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." |
|||
} |
|||
} |
|||
@ -1,27 +1,38 @@ |
|||
# COMMON PATHS |
|||
$full = $args[0] |
|||
|
|||
# COMMON PATHS |
|||
|
|||
$rootFolder = (Get-Item -Path "./" -Verbose).FullName |
|||
|
|||
# List of solutions |
|||
# List of solutions used only in development mode |
|||
$solutionPaths = @( |
|||
"../framework", |
|||
"../modules/users", |
|||
"../modules/permission-management", |
|||
"../modules/setting-management", |
|||
"../modules/feature-management", |
|||
"../modules/identity", |
|||
"../modules/identityserver", |
|||
"../modules/tenant-management", |
|||
"../modules/audit-logging", |
|||
"../modules/background-jobs", |
|||
"../modules/account" |
|||
) |
|||
|
|||
$solutionPaths = ( |
|||
"../framework", |
|||
"../modules/users", |
|||
"../modules/permission-management", |
|||
"../modules/setting-management", |
|||
"../modules/feature-management", |
|||
"../modules/identity", |
|||
"../modules/identityserver", |
|||
"../modules/tenant-management", |
|||
"../modules/account", |
|||
"../modules/docs", |
|||
"../modules/blogging", |
|||
"../modules/audit-logging", |
|||
"../modules/background-jobs", |
|||
"../modules/client-simulation", |
|||
"../modules/virtual-file-explorer", |
|||
"../templates/module/aspnet-core", |
|||
"../templates/app/aspnet-core", |
|||
"../samples/MicroserviceDemo", |
|||
"../abp_io/AbpIoLocalization" |
|||
) |
|||
if ($full -eq "-f") |
|||
{ |
|||
# List of additional solutions required for full build |
|||
$solutionPaths += ( |
|||
"../modules/client-simulation", |
|||
"../modules/virtual-file-explorer", |
|||
"../modules/docs", |
|||
"../modules/blogging", |
|||
"../templates/module/aspnet-core", |
|||
"../templates/app/aspnet-core", |
|||
"../abp_io/AbpIoLocalization" |
|||
) |
|||
}else{ |
|||
Write-host "" |
|||
Write-host ":::::::::::::: !!! You are in development mode !!! ::::::::::::::" -ForegroundColor red -BackgroundColor yellow |
|||
Write-host "" |
|||
} |
|||
|
|||
@ -0,0 +1,30 @@ |
|||
# Social/External Logins |
|||
|
|||
## ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
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. |
|||
@ -0,0 +1,78 @@ |
|||
# BLOB Storing Aliyun Provider |
|||
|
|||
BLOB Storing Aliyun Provider can store BLOBs in [Aliyun Blob storage](https://help.aliyun.com/product/31815.html). |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Aliyun BLOB as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun) 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.BlobStoring.Aliyun` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.Aliyun` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringAliyunModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the Aliyun storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseAliyun(aliyun => |
|||
{ |
|||
aliyun.AccessKeyId = "your aliyun access key id"; |
|||
aliyun.AccessKeySecret = "your aliyun access key secret"; |
|||
aliyun.Endpoint = "your oss endpoint"; |
|||
aliyun.RegionId = "your sts region id"; |
|||
aliyun.RoleArn = "the arn of ram role"; |
|||
aliyun.RoleSessionName = "the name of the certificate"; |
|||
aliyun.Policy = "policy"; |
|||
aliyun.DurationSeconds = "expiration date"; |
|||
aliyun.ContainerName = "your aliyun container name"; |
|||
aliyun.CreateContainerIfNotExists = false; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **AccessKeyId** ([NotNull]string): AccessKey is the key to access the Alibaba Cloud API. It has full permissions for the account. Please keep it safe! Recommend to follow [Alibaba Cloud security best practicess](https://help.aliyun.com/document_detail/102600.html),Use RAM sub-user AccessKey to call API. |
|||
* **AccessKeySecret** ([NotNull]string): Same as above. |
|||
* **Endpoint** ([NotNull]string): Endpoint is the external domain name of OSS. See the [document](https://help.aliyun.com/document_detail/31837.html) for details. |
|||
* **UseSecurityTokenService** (bool): Use [STS temporary credentials](https://help.aliyun.com/document_detail/100624.html) to access OSS services,default: `false`. |
|||
* **RegionId** (string): Access address of STS service. See the [document](https://help.aliyun.com/document_detail/66053.html) for details. |
|||
* **RoleArn** ([NotNull]string): STS required role ARN. See the [document](https://help.aliyun.com/document_detail/100624.html) for details. |
|||
* **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish. |
|||
* **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details. |
|||
* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. |
|||
* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): |
|||
* Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. |
|||
* Container names Must start and end with lowercase letters and numbers. |
|||
* Container names must be from **3** through **63** characters long. |
|||
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aliyun, `AliyunBlobProvider` will try to create it. |
|||
* **TemporaryCredentialsCacheKey** (bool): The cache key of STS credentials. |
|||
|
|||
|
|||
## Aliyun Blob Name Calculator |
|||
|
|||
Aliyun Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: |
|||
|
|||
* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` string if current tenant is not `null`. |
|||
* Appends the BLOB name. |
|||
|
|||
## Other Services |
|||
|
|||
* `AliyunBlobProvider` is the main service that implements the Aliyun BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AliyunBlobProvider` class). |
|||
* `IAliyunBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAliyunBlobNameCalculator` by default. |
|||
* `IOssClientFactory` is used create OSS client. It is implemented by the `DefaultOssClientFactory` by default. You can override/replace it,if you want customize. |
|||
@ -0,0 +1,85 @@ |
|||
# BLOB Storing Aws Provider |
|||
|
|||
BLOB Storing Aws Provider can store BLOBs in [Amazon Simple Storage Service](https://aws.amazon.com/s3/). |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Aws BLOB as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws) 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.BlobStoring.Aws` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.Aws` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringAwsModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the Aws storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseAws(Aws => |
|||
{ |
|||
Aws.AccessKeyId = "your Aws access key id"; |
|||
Aws.SecretAccessKey = "your Aws access key secret"; |
|||
Aws.UseCredentials = "set true to use credentials"; |
|||
Aws.UseTemporaryCredentials = "set true to use temporary credentials"; |
|||
Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; |
|||
Aws.ProfileName = "the name of the profile to get credentials from"; |
|||
Aws.ProfilesLocation = "the path to the aws credentials file to look at"; |
|||
Aws.Region = "the system name of the service"; |
|||
Aws.Name = "the name of the federated user"; |
|||
Aws.Policy = "policy"; |
|||
Aws.DurationSeconds = "expiration date"; |
|||
Aws.ContainerName = "your Aws container name"; |
|||
Aws.CreateContainerIfNotExists = false; |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **AccessKeyId** (string): AWS Access Key ID. |
|||
* **SecretAccessKey** (string): AWS Secret Access Key. |
|||
* **UseCredentials** (bool): Use [credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html) to access AWS services,default : `false`. |
|||
* **UseTemporaryCredentials** (bool): Use [temporary credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempSessionToken.html) to access AWS services,default : `false`. |
|||
* **UseTemporaryFederatedCredentials** (bool): Use [federated user temporary credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempFederationToken.html) to access AWS services, default : `false`. |
|||
* **ProfileName** (string): The [name of the profile]((https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html)) to get credentials from. |
|||
* **ProfilesLocation** (string): The path to the aws credentials file to look at. |
|||
* **Region** (string): The system name of the service. |
|||
* **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy. |
|||
* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using subaccounts operated OSS,if the value is 0. |
|||
* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): |
|||
* Bucket names must be between **3** and **63** characters long. |
|||
* Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). |
|||
* Bucket names must begin and end with a letter or number. |
|||
* Bucket names must not be formatted as an IP address (for example, 192.168.5.4). |
|||
* Bucket names can't begin with **xn--** (for buckets created after February 2020). |
|||
* Bucket names must be unique within a partition. |
|||
* Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration. |
|||
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aws, `AwsBlobProvider` will try to create it. |
|||
|
|||
## Aws Blob Name Calculator |
|||
|
|||
Aws Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: |
|||
|
|||
* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` string if current tenant is not `null`. |
|||
* Appends the BLOB name. |
|||
|
|||
## Other Services |
|||
|
|||
* `AwsBlobProvider` is the main service that implements the Aws BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AwsBlobProvider` class). |
|||
* `IAwsBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAwsBlobNameCalculator` by default. |
|||
* `IAmazonS3ClientFactory` is used create OSS client. It is implemented by the `DefaultAmazonS3ClientFactory` by default. You can override/replace it,if you want customize. |
|||
@ -0,0 +1,69 @@ |
|||
# BLOB Storing Minio Provider |
|||
|
|||
BLOB Storing Minio Provider can store BLOBs in [MinIO Object storage](https://min.io/). |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Minio BLOB as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) 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.BlobStoring.Minio` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.Minio` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringMinioModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the minio storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMinio(minio => |
|||
{ |
|||
minio.EndPoint = "your minio endPoint"; |
|||
minio.AccessKey = "your minio accessKey"; |
|||
minio.SecretKey = "your minio secretKey"; |
|||
minio.BucketName = "your minio bucketName"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html |
|||
* **AccessKey** (string): Access key is the user ID that uniquely identifies your account. |
|||
* **SecretKey** (string): Secret key is the password to your account. |
|||
* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: |
|||
* Bucket names must be between **3** and **63** characters long. |
|||
* Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). |
|||
* Bucket names must begin and end with a letter or number. |
|||
* Bucket names must not be formatted as an IP address (for example, 192.168.5.4). |
|||
* Bucket names can't begin with **xn--** (for buckets created after February 2020). |
|||
* Bucket names must be unique within a partition. |
|||
* Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration. |
|||
* **WithSSL** (bool): Default value is `false`,Chain to MinIO Client object to use https instead of http. |
|||
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a bucket does not exist in minio, `MinioBlobProvider` will try to create it. |
|||
|
|||
|
|||
## Minio Blob Name Calculator |
|||
|
|||
Minio Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: |
|||
|
|||
* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` string if current tenant is not `null`. |
|||
* Appends the BLOB name. |
|||
|
|||
## Other Services |
|||
|
|||
* `MinioBlobProvider` is the main service that implements the Minio BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `MinioBlobProvider` class). |
|||
* `IMinioBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultMinioBlobNameCalculator` by default. |
|||
@ -0,0 +1,177 @@ |
|||
# ABP Framework v3.0 Has Been Released |
|||
|
|||
We are excited to announce that the **ABP Framework** & and the **ABP Commercial** version 3.0 have been released. As different than the regular release lifecycle, which is 2-weeks, this version has taken 4-weeks with **119 [issues](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3A3.0)** closed, **89 [pull requests](https://github.com/abpframework/abp/pulls?q=is%3Aopen+is%3Apr+milestone%3A3.0)** merged and **798 commits** done in the main framework [repository](https://github.com/abpframework/abp). |
|||
|
|||
Since this is a **major version**, it also includes some **breaking changes**. Don't panic, the changes are easy to adapt and will be explained below. |
|||
|
|||
> See the [GitHub release notes](https://github.com/abpframework/abp/releases/tag/3.0.0) for a detailed change log. |
|||
|
|||
## What's New with the ABP Framework 3.0? |
|||
|
|||
This post will only cover the important features/changes. You can see all the changes on the [GitHub release notes](https://github.com/abpframework/abp/releases/tag/2.9.0). |
|||
|
|||
### Angular 10! |
|||
|
|||
Angular version 10 has just been [released](https://blog.angular.io/version-10-of-angular-now-available-78960babd41) and we've immediately migrated the [startup templates](https://docs.abp.io/en/abp/latest/Startup-Templates/Application) to Angular 10! So, when you [create a new solution](https://abp.io/get-started) with the Angular UI, you will take the advantage of the new Angular. |
|||
|
|||
We've prepared a [migration guide](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md) for the projects created an older version and want to migrate to Angular 10. |
|||
|
|||
### The Oracle Integration Package |
|||
|
|||
We had created [an integration package](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) for the Oracle for EF Core based applications using the Devart's library since the official Oracle EF Core package was not supporting the EF Core 3.1. It now supports as a [beta release](https://www.nuget.org/packages/Oracle.EntityFrameworkCore/3.19.0-beta1). While it is in beta, we've created [the integration package](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle), so you can use it in your application. |
|||
|
|||
See [the documentation](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle) for details. |
|||
|
|||
### Azure BLOB Storage Provider |
|||
|
|||
We had created a [BLOB storing system](https://docs.abp.io/en/abp/latest/Blob-Storing) in the previous version with a file system and database storage provider. This release introduces the Azure BLOB Storage provider. See [the documentation](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure). |
|||
|
|||
### Distributed Cache Bulk Operations & the New Redis Cache Package |
|||
|
|||
The [standard IDistributeCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) interface of the ASP.NET Core doesn't contain **bulk operations**, like setting multiple items with a single method/server call. ABP Framework introduces new methods those can be used for bulk operations on the ABP's `IDistributedCache<T>` interface: |
|||
|
|||
* GetManyAsync / GetMany |
|||
* SetManyAsync / SetMany |
|||
|
|||
Then we needed to implement these new methods for Redis cache and [had to create](https://github.com/abpframework/abp/issues/4483) a Redis integration package which extends the Microsoft's implementation. |
|||
|
|||
These methods are also used by the ABP Framework to cache settings, features and permissions for a user/role/tenant and brings a **significant performance improvement**. |
|||
|
|||
See the [caching document](https://docs.abp.io/en/abp/latest/Caching) for details. |
|||
|
|||
### Embedded Files Manifest Support for the Virtual File System |
|||
|
|||
Virtual File System now supports to use `GenerateEmbeddedFilesManifest` in your projects to add the **real file/directory structure** of your embedded resources in the compiled assembly. So, you can now access to the files without any file name restriction (previously, some special chars like `.` in the directory names was a problem in some cases) |
|||
|
|||
See [the documentation](https://docs.abp.io/en/abp/latest/Virtual-File-System) to learn how to take the advantage of new system. |
|||
|
|||
### New Samples |
|||
|
|||
Based on the requests from the community, we've prepared two new sample applications: |
|||
|
|||
* [StoredProcedureDemo](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo) demonstrates how to call stored procedures, views and functions inside a custom repository. |
|||
* [OrganizationUnitSample](https://github.com/abpframework/abp-samples/tree/master/OrganizationUnitSample) shows how to use the organization unit system of the [Identity module](https://docs.abp.io/en/abp/latest/Modules/Identity) for your entities. |
|||
|
|||
### DynamicStringLength & DynamicMaxLength Attributes |
|||
|
|||
The standard `StringLength` and `MaxLength` data annotation attributes is useful to validate properties of a class when the class is used as a Model or [DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects). However, just like any other attribute, the length values should be literal (constant) values known at **compile time**. |
|||
|
|||
**Example: Using the `StringLength`** |
|||
|
|||
````csharp |
|||
public class CreateBookDto |
|||
{ |
|||
public const int MaxNameLength = 128; //CONSTANT! |
|||
|
|||
[StringLength(MaxNameLength)] |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
ABP Framework now has the `DynamicStringLength` & `DynamicMaxLength` properties to allow to determine the lengths at **runtime**. |
|||
|
|||
**Example: Using the `DynamicStringLength`** |
|||
|
|||
````csharp |
|||
public class CreateBookDto |
|||
{ |
|||
public static int MaxNameLength { get; set; } = 128; |
|||
|
|||
[DynamicStringLength(typeof(CreateBookDto), nameof(MaxNameLength))] |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
`DynamicStringLength` gets a class **type** and the **name** of a static property on this class to read the max length (there is also a minimum length option just like the `StringLength`). |
|||
|
|||
This allows you to get the max value from a configuration and set on the application startup (generally, in the `PreConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics)): |
|||
|
|||
````csharp |
|||
CreateBookDto.MaxNameLength = 200; |
|||
```` |
|||
|
|||
This feature is used by the [pre-built application modules](https://docs.abp.io/en/abp/latest/Modules/Index), so you can now override the max lengths of the properties defined in these modules. |
|||
|
|||
### Auto Distributed Events |
|||
|
|||
ABP can **automatically publish distributed events** for all entities on their create, update and delete events. That's pretty useful since you commonly interest in these basic events in a distributed system. |
|||
|
|||
This feature is **mature and [documented](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus#pre-defined-events)** with the v3.0. You can easily configure some or all the entities to be published. |
|||
|
|||
### IAsyncQueryableExecuter |
|||
|
|||
When you work with LINQ extension methods, you need to call `ToListAsync()`, `FirstOrDefaultAsync()`... methods on your queries. Unfortunately, these methods are **not standard** LINQ extension methods. They are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) package (or in the [MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver/) package if you are using the MongoDB). |
|||
|
|||
So, you need to depend on this package if you want to use the async extension methods. That breaks the layering and makes your application or domain layer depends on the EF Core / MongoDB package. |
|||
|
|||
`IAsyncQueryableExecuter` is a service defined by the ABP Framework to **execute queries asynchronously without depending the specific provider** (EF Core / MongoDB) package. |
|||
|
|||
See [the documentation](https://docs.abp.io/en/abp/latest/Repositories#iqueryable-async-operations) to read the details and learn our recommendations. |
|||
|
|||
### API documentation |
|||
|
|||
We are now publishing [API documents](https://docs.abp.io/api-docs/abp/2.9/api/index.html) for the ABP Framework and modules in every release. So, you can explore the ABP Framework classes much more easier than before. Click the the **API Documentation** link on the navigation menu of the [documentation](https://docs.abp.io/en/abp/latest/). |
|||
|
|||
### Package List |
|||
|
|||
We have [created a page](http://abp.io/packages) to list all the ABP-related official NuGet and NPM packages. |
|||
|
|||
### Others |
|||
|
|||
* Implemented front-channel server-side clients [sign out](https://identityserver4.readthedocs.io/en/latest/topics/signout.html) for the identity server. |
|||
* `abp.currentUser` (`CurrentUser` service in the Angular UI) now has a `roles` array that contains role names of the current user. |
|||
* Upgraded all the NuGet and NPM package dependencies. |
|||
* Introduced `ReadOnlyAppService` base class (which has only the get operations) in addition to the `CrudAppService` base class (which has get, create, update and delete operations). |
|||
|
|||
See the [GitHub release notes](https://github.com/abpframework/abp/releases/tag/3.0.0) for others updates. |
|||
|
|||
## What's New with the ABP Commercial 3.0? |
|||
|
|||
In addition to all the features coming with the ABP Framework, the ABP Commercial has additional features with this release, as always. This section covers the [ABP Commercial](https://commercial.abp.io/) highlights in the version 2.9. |
|||
|
|||
### New File Management Module |
|||
|
|||
We've created a new module that is used to store and manage files in your application. This new module is based on the [BLOB Storing system](https://docs.abp.io/en/abp/latest/Blob-Storing), so it can use different storage providers to store the file contents. |
|||
|
|||
**Example screenshot** |
|||
|
|||
 |
|||
|
|||
You can upload, download and organize files in a hierarchical folder structure. It is also compatible to multi-tenancy and you can determine total size limit for your tenants. In the next versions, we will be working on a "share" system to share files between users in a more controlled way or share your files with your customers with a public link. |
|||
|
|||
> File Management module is currently available only for the MVC / Razor Pages UI. We are working on the Angular UI and it will be released in the next versions. |
|||
|
|||
## Breaking Changes |
|||
|
|||
Since this is a major version, we've redesigned some APIs and introduced a few "easy to fix" breaking changes. |
|||
|
|||
### ABP Framework |
|||
|
|||
* Changed some **consts** in the pre-built application modules to static properties that is possible to change by your code. If you've used these consts on an attribute, then use the `DynamicStringLength` as explained above. |
|||
* Changed `ConcurrencyStamp` max length to 40. You need to **add a database migration** and update your database after upgrading the ABP Framework. |
|||
* Using `~` instead of `^` for NPM package dependencies anymore, to be more stable. |
|||
|
|||
### ABP Commercial |
|||
|
|||
* Changed file names for the application logos. Previously, it was using separate logo files for each theme, like `theme1.png`, `theme1-reverse.png`, `theme2.png`, `theme2-reverse.png` (... `6`). Now, we have only two logo files: `logo-light.png` and `logo-dark.png`. So, rename your logo in the `wwwroot/images/logo/` folder for the MVC UI and `/src/assets/images/logo/` folder for the Angular UI. |
|||
* We've added the [API documentation](https://docs.abp.io/api-docs/commercial/2.9/api/index.html) for the ABP Commercial too. |
|||
|
|||
> **Also, see the [migration guide](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md) for Angular UI**. |
|||
|
|||
## Known Issues |
|||
|
|||
* 3.0.0 version has a problem with tiered architecture. See [this issue](https://github.com/abpframework/abp/pull/4564) to fix it for your application until we release the v3.0.1. |
|||
|
|||
## About the Next Versions |
|||
|
|||
We will continue to release a new minor/feature version in every two weeks. So, the next expected release date is **2020-07-16** for the version **3.1**. |
|||
|
|||
In the next few versions, we will be focused on the **Blazor UI**, as promised on [the road map](https://docs.abp.io/en/abp/latest/Road-Map). We will continue to improve the documentation, create samples, add other new features and enhancements. Follow the [ABP Framework Twitter account](https://twitter.com/abpframework) for the latest news... |
|||
|
|||
## Bonus: Articles! |
|||
|
|||
Beside developing our products, our team are constantly writing articles/tutorials on various topics. You may want to check the latest articles: |
|||
|
|||
* [What is New in Angular 10?](https://volosoft.com/blog/what-is-new-in-angular-10) |
|||
* [Real-Time Messaging In A Distributed Architecture Using ABP, SignalR & RabbitMQ](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) |
|||
* [How to Use Attribute Directives to Avoid Repetition in Angular Templates](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) |
|||
|
After Width: | Height: | Size: 116 KiB |
@ -0,0 +1,96 @@ |
|||
# How to Customize the SignIn Manager for ABP Applications |
|||
|
|||
After creating a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) for SignIn Manager and the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). |
|||
|
|||
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. |
|||
|
|||
This document explains how to customize the SignIn Manager for your own application. |
|||
|
|||
## Create a CustomSignInManager |
|||
|
|||
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. |
|||
|
|||
````csharp |
|||
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
|||
{ |
|||
public CustomSignInManager( |
|||
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
|||
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
|||
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
|||
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
|||
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
|||
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
|||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. |
|||
|
|||
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. |
|||
|
|||
## Overriding the GetExternalLoginInfoAsync Method |
|||
|
|||
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. |
|||
|
|||
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. |
|||
|
|||
````csharp |
|||
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
|||
{ |
|||
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
|||
var items = auth?.Properties?.Items; |
|||
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (expectedXsrf != null) |
|||
{ |
|||
if (!items.ContainsKey(XsrfKey)) |
|||
{ |
|||
return null; |
|||
} |
|||
var userId = items[XsrfKey] as string; |
|||
if (userId != expectedXsrf) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
|||
var provider = items[LoginProviderKey] as string; |
|||
if (providerKey == null || provider == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
|||
?? provider; |
|||
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
|||
{ |
|||
AuthenticationTokens = auth.Properties.GetTokens() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](https://docs.abp.io/en/abp/latest/Dependency-Injection). |
|||
|
|||
## Register to Dependency Injection |
|||
|
|||
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). |
|||
|
|||
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: |
|||
|
|||
````csharp |
|||
PreConfigure<IdentityBuilder>(identityBuilder => |
|||
{ |
|||
identityBuilder.AddSignInManager<CustomSignInManager>(); |
|||
}); |
|||
```` |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). |
|||
@ -0,0 +1,207 @@ |
|||
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications |
|||
|
|||
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. |
|||
|
|||
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. |
|||
|
|||
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. |
|||
|
|||
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. |
|||
|
|||
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. |
|||
|
|||
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. |
|||
> |
|||
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. |
|||
|
|||
## 1. AddAzureAD |
|||
|
|||
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). |
|||
|
|||
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. |
|||
|
|||
#### **Updating `appsettings.json`** |
|||
|
|||
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. |
|||
|
|||
Then, you need to configure the `OpenIdConnectOptions` to complete the integration. |
|||
|
|||
#### Configuring OpenIdConnectOptions |
|||
|
|||
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "Acme.BookStore"; |
|||
}) |
|||
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
|||
|
|||
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
|||
{ |
|||
options.Authority = options.Authority + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.RequireHttpsMetadata = false; |
|||
|
|||
options.TokenValidationParameters.ValidateIssuer = false; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
options.SaveTokens = true; |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
> **Don't forget to:** |
|||
> |
|||
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. |
|||
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
|||
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
|||
|
|||
You are done and integration is completed. |
|||
|
|||
## 2. Alternative Approach: AddOpenIdConnect |
|||
|
|||
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. |
|||
|
|||
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. |
|||
|
|||
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
|
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "BookStore"; |
|||
}) |
|||
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
|||
{ |
|||
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.SaveTokens = true; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). |
|||
|
|||
# FAQ |
|||
|
|||
* Help! `GetExternalLoginInfoAsync` returns `null`! |
|||
|
|||
* There can be 2 reasons for this; |
|||
|
|||
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: |
|||
|
|||
````csharp |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
```` |
|||
|
|||
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: |
|||
|
|||
````csharp |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
```` |
|||
|
|||
|
|||
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! |
|||
|
|||
|
|||
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add |
|||
|
|||
````csharp |
|||
options.Scope.Add("email"); |
|||
```` |
|||
|
|||
to your openid configuration. |
|||
|
|||
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! |
|||
|
|||
* If you set your **CallbackPath** in appsettings as: |
|||
|
|||
````csharp |
|||
"AzureAd": { |
|||
... |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
your **Redirect URI** of your application in azure portal must be <u>with domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. |
|||
|
|||
* Help! I keep getting ***AADSTS700051: The response_type 'token' is not enabled for the application.*** error! |
|||
|
|||
* This error occurs when you request **token** (access token) along with **id_token** without enabling Access tokens on Azure portal app registrations. Simply tick **Access tokens** checkbox located on top of ID tokens to be able to request token aswell. |
|||
|
|||
* Help! I keep getting ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** error! |
|||
|
|||
* This error occurs when you request **code** along with **id_token**. You need to add **client secret** on azure portal app registrations, under **Certificates & secrets** menu. Afterwards, you need to add openid configuration option like: |
|||
|
|||
````csharp |
|||
options.ClientSecret = "Value of your secret on azure portal"; |
|||
```` |
|||
|
|||
* How can I **debug/watch** which claims I get before they get mapped? |
|||
|
|||
* You can add a simple event under openid configuration to debug before mapping like: |
|||
|
|||
````csharp |
|||
options.Events.OnTokenValidated = (async context => |
|||
{ |
|||
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
|||
await Task.CompletedTask; |
|||
}); |
|||
```` |
|||
@ -0,0 +1,108 @@ |
|||
# How to Customize the Login Page for MVC / Razor Page Applications |
|||
|
|||
When you create a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. |
|||
|
|||
This document explains how to customize the login page for your own application. |
|||
|
|||
## Create a Login PageModel |
|||
|
|||
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. |
|||
|
|||
````csharp |
|||
public class CustomLoginModel : LoginModel |
|||
{ |
|||
public CustomLoginModel( |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
|||
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
|||
: base(schemeProvider, accountOptions) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](https://docs.abp.io/en/abp/latest/Dependency-Injection) system. |
|||
|
|||
Then you can override any method you need and add new methods and properties needed by the UI. |
|||
|
|||
## Overriding the Login Page UI |
|||
|
|||
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module. |
|||
|
|||
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: |
|||
|
|||
````xml |
|||
@page |
|||
@using Volo.Abp.Account.Settings |
|||
@using Volo.Abp.Settings |
|||
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
|||
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
|||
@if (Model.EnableLocalLogin) |
|||
{ |
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Login"]</h4> |
|||
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
<strong> |
|||
@L["AreYouANewUser"] |
|||
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
|||
</strong> |
|||
} |
|||
<form method="post" class="mt-4"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
|||
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.Password"></label> |
|||
<input asp-for="LoginInput.Password" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-check"> |
|||
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
|||
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
|||
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
|||
</label> |
|||
</div> |
|||
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
|||
</form> |
|||
</div> |
|||
|
|||
<div class="card-footer text-center border-0"> |
|||
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
@if (Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="col-md-6"> |
|||
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="alert alert-warning"> |
|||
<strong>@L["InvalidLoginRequest"]</strong> |
|||
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). |
|||
@ -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,276 @@ |
|||
# Implementing Passwordless Authentication in ASP.NET Core Identity |
|||
|
|||
## Introduction |
|||
|
|||
To allow a user login with a magic URL, you need to implement a custom token provider. In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering a password. |
|||
|
|||
### Source Code |
|||
|
|||
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). |
|||
|
|||
## Creating the Solution |
|||
|
|||
Before starting to 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 |
|||
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using PasswordlessAuthentication.Localization |
|||
@using PasswordlessAuthentication.Web.Menus |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Layout |
|||
@model PasswordlessAuthentication.Web.Pages.IndexModel |
|||
|
|||
@{ |
|||
ViewBag.PageTitle = "Home"; |
|||
} |
|||
@inject IPageLayout PageLayout; |
|||
@{ |
|||
PageLayout.Content.Title = L["Home"].Value; |
|||
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value); |
|||
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.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"> |
|||
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl) |
|||
</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 [the 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/ |
|||
@ -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). |
|||
@ -1,212 +0,0 @@ |
|||
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications |
|||
|
|||
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. |
|||
|
|||
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. |
|||
|
|||
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. |
|||
|
|||
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. |
|||
|
|||
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. |
|||
|
|||
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. |
|||
> |
|||
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. |
|||
|
|||
## 1. AddAzureAD |
|||
|
|||
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). |
|||
|
|||
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. |
|||
|
|||
#### **Updating `appsettings.json`** |
|||
|
|||
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. |
|||
|
|||
Then, you need to configure the `OpenIdConnectOptions` to complete the integration. |
|||
|
|||
#### Configuring OpenIdConnectOptions |
|||
|
|||
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "Acme.BookStore"; |
|||
}) |
|||
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
|||
|
|||
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
|||
{ |
|||
options.Authority = options.Authority + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.RequireHttpsMetadata = false; |
|||
|
|||
options.TokenValidationParameters.ValidateIssuer = false; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
options.SaveTokens = true; |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
> **Don't forget to:** |
|||
> |
|||
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. |
|||
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. |
|||
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
|||
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
|||
|
|||
You are done and integration is completed. |
|||
|
|||
## 2. Alternative Approach: AddOpenIdConnect |
|||
|
|||
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. |
|||
|
|||
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. |
|||
|
|||
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: |
|||
|
|||
````json |
|||
"AzureAd": { |
|||
"Instance": "https://login.microsoftonline.com/", |
|||
"TenantId": "<your-tenant-id>", |
|||
"ClientId": "<your-client-id>", |
|||
"Domain": "domain.onmicrosoft.com", |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: |
|||
|
|||
````csharp |
|||
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
|||
{ |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
|
|||
context.Services.AddAuthentication() |
|||
.AddIdentityServerAuthentication(options => |
|||
{ |
|||
options.Authority = configuration["AuthServer:Authority"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.ApiName = "BookStore"; |
|||
}) |
|||
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
|||
{ |
|||
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
|||
options.ClientId = configuration["AzureAd:ClientId"]; |
|||
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
|||
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
|||
options.RequireHttpsMetadata = false; |
|||
options.SaveTokens = true; |
|||
options.GetClaimsFromUserInfoEndpoint = true; |
|||
|
|||
options.Scope.Add("email"); |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
# FAQ |
|||
|
|||
* Help! `GetExternalLoginInfoAsync` returns `null`! |
|||
|
|||
* There can be 2 reasons for this; |
|||
|
|||
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: |
|||
|
|||
````csharp |
|||
options.SignInScheme = IdentityConstants.ExternalScheme; |
|||
```` |
|||
|
|||
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: |
|||
|
|||
````csharp |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
|||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
|||
```` |
|||
|
|||
|
|||
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! |
|||
|
|||
|
|||
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add |
|||
|
|||
````csharp |
|||
options.Scope.Add("email"); |
|||
```` |
|||
|
|||
to your openid configuration. |
|||
|
|||
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! |
|||
|
|||
* If you set your **CallbackPath** in appsettings as: |
|||
|
|||
````csharp |
|||
"AzureAd": { |
|||
... |
|||
"CallbackPath": "/signin-azuread-oidc" |
|||
} |
|||
```` |
|||
|
|||
your **Redirect URI** of your application in azure portal must be <u>with domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. |
|||
|
|||
* Help! I keep getting ***AADSTS700051: The response_type 'token' is not enabled for the application.*** error! |
|||
|
|||
* This error occurs when you request **token** (access token) along with **id_token** without enabling Access tokens on Azure portal app registrations. Simply tick **Access tokens** checkbox located on top of ID tokens to be able to request token aswell. |
|||
|
|||
* Help! I keep getting ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** error! |
|||
|
|||
* This error occurs when you request **code** along with **id_token**. You need to add **client secret** on azure portal app registrations, under **Certificates & secrets** menu. Afterwards, you need to add openid configuration option like: |
|||
|
|||
````csharp |
|||
options.ClientSecret = "Value of your secret on azure portal"; |
|||
```` |
|||
|
|||
* How can I **debug/watch** which claims I get before they get mapped? |
|||
|
|||
* You can add a simple event under openid configuration to debug before mapping like: |
|||
|
|||
````csharp |
|||
options.Events.OnTokenValidated = (async context => |
|||
{ |
|||
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
|||
await Task.CompletedTask; |
|||
}); |
|||
```` |
|||
|
|||
## See Also |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
|||
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). |
|||
@ -1,112 +0,0 @@ |
|||
# How to Customize the Login Page for MVC / Razor Page Applications |
|||
|
|||
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. |
|||
|
|||
This document explains how to customize the login page for your own application. |
|||
|
|||
## Create a Login PageModel |
|||
|
|||
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. |
|||
|
|||
````csharp |
|||
public class CustomLoginModel : LoginModel |
|||
{ |
|||
public CustomLoginModel( |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
|||
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
|||
: base(schemeProvider, accountOptions) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. |
|||
|
|||
Then you can override any method you need and add new methods and properties needed by the UI. |
|||
|
|||
## Overriding the Login Page UI |
|||
|
|||
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module. |
|||
|
|||
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: |
|||
|
|||
````xml |
|||
@page |
|||
@using Volo.Abp.Account.Settings |
|||
@using Volo.Abp.Settings |
|||
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
|||
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
|||
@if (Model.EnableLocalLogin) |
|||
{ |
|||
<div class="card mt-3 shadow-sm rounded"> |
|||
<div class="card-body p-5"> |
|||
<h4>@L["Login"]</h4> |
|||
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
|||
{ |
|||
<strong> |
|||
@L["AreYouANewUser"] |
|||
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
|||
</strong> |
|||
} |
|||
<form method="post" class="mt-4"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
|||
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label asp-for="LoginInput.Password"></label> |
|||
<input asp-for="LoginInput.Password" class="form-control" /> |
|||
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
|||
</div> |
|||
<div class="form-check"> |
|||
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
|||
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
|||
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
|||
</label> |
|||
</div> |
|||
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
|||
</form> |
|||
</div> |
|||
|
|||
<div class="card-footer text-center border-0"> |
|||
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
@if (Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="col-md-6"> |
|||
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
|||
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
|||
<input asp-for="ReturnUrl" /> |
|||
<input asp-for="ReturnUrlHash" /> |
|||
@foreach (var provider in Model.VisibleExternalProviders) |
|||
{ |
|||
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
|||
} |
|||
</form> |
|||
</div> |
|||
} |
|||
|
|||
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
|||
{ |
|||
<div class="alert alert-warning"> |
|||
<strong>@L["InvalidLoginRequest"]</strong> |
|||
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
## See Also |
|||
|
|||
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). |
|||
@ -1,101 +0,0 @@ |
|||
# How to Customize the SignIn Manager for ABP Applications |
|||
|
|||
After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). |
|||
|
|||
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. |
|||
|
|||
This document explains how to customize the SignIn Manager for your own application. |
|||
|
|||
## Create a CustomSignInManager |
|||
|
|||
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. |
|||
|
|||
````csharp |
|||
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
|||
{ |
|||
public CustomSignInManager( |
|||
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
|||
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
|||
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
|||
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
|||
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
|||
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
|||
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
|||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
|||
{ |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. |
|||
|
|||
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. |
|||
|
|||
## Overriding the GetExternalLoginInfoAsync Method |
|||
|
|||
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. |
|||
|
|||
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. |
|||
|
|||
````csharp |
|||
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
|||
{ |
|||
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
|||
var items = auth?.Properties?.Items; |
|||
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (expectedXsrf != null) |
|||
{ |
|||
if (!items.ContainsKey(XsrfKey)) |
|||
{ |
|||
return null; |
|||
} |
|||
var userId = items[XsrfKey] as string; |
|||
if (userId != expectedXsrf) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
|||
var provider = items[LoginProviderKey] as string; |
|||
if (providerKey == null || provider == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
|||
?? provider; |
|||
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
|||
{ |
|||
AuthenticationTokens = auth.Properties.GetTokens() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). |
|||
|
|||
## Register to Dependency Injection |
|||
|
|||
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). |
|||
|
|||
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: |
|||
|
|||
````csharp |
|||
PreConfigure<IdentityBuilder>(identityBuilder => |
|||
{ |
|||
identityBuilder.AddSignInManager<CustomSignInManager>(); |
|||
}); |
|||
```` |
|||
|
|||
## The Source Code |
|||
|
|||
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
|||
|
|||
## See Also |
|||
|
|||
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
|||
* [Identity Management Module](../Modules/Identity.md). |
|||
@ -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) |
|||
|
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,135 @@ |
|||
# Multi Tenancy in Angular UI |
|||
|
|||
ABP Angular UI supports the multi-tenancy. The following features related to multi-tenancy are available in the startup templates. |
|||
|
|||
|
|||
 |
|||
<p style="font-size:small;text-align:center;">Tenants page</p> |
|||
|
|||
On the page above, you can; |
|||
|
|||
- See the all tenants. |
|||
- Create a new tenant. |
|||
- Edit an existing tenant. |
|||
- Delete a tenant. |
|||
|
|||
|
|||
 |
|||
<p style="font-size:small;text-align:center;">Tenant Switching Component</p> |
|||
|
|||
You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id to the backend as `__tenant` header on each request. |
|||
|
|||
|
|||
## Domain Tenant Resolver |
|||
|
|||
Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: |
|||
|
|||
Subdomain resolver: |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.mydomain.com/' |
|||
}, |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
**{0}** is the placeholder to determine current tenant's unique name. |
|||
|
|||
After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get the tenant name as **mytenant1**. Then, the app will call the `/api/abp/multi-tenancy/tenants/by-name/mytenant1` endpoint to check if the tenant exists. If the tenant (mytenant1) exists, the app will keep this tenant data and send its `id` as `__tenant` header to the backend on each request. If the tenant does not exist, the app will not send `__tenant` header to the backend. |
|||
|
|||
> **Important Note:** If you define the `baseUrl` with the placeholder (**{0}**), the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page) will be hidden. |
|||
|
|||
|
|||
Domain resolver: |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.com/' |
|||
}, |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
After the configuration above, if your app runs on the `mytenant.com`, the app will get the tenant name as **mytenant**. |
|||
|
|||
### Tenant Specific Remote Endpoints |
|||
|
|||
The **{0}** placeholder can be put to the API URLs in the environment to determine tenant specific endpoints. |
|||
|
|||
```js |
|||
// environment.prod.ts |
|||
|
|||
export const environment = { |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://{0}.mydomain.com/', |
|||
//... |
|||
}, |
|||
oAuthConfig: { |
|||
issuer: 'https://{0}.ids.mydomain.com', |
|||
//... |
|||
}, |
|||
apis: { |
|||
default: { |
|||
url: 'https://{0}.api.mydomain.com', |
|||
}, |
|||
AbpIdentity: { |
|||
url: 'https://{0}.identity.mydomain.com', |
|||
}, |
|||
}, |
|||
} |
|||
``` |
|||
|
|||
> **Important Note:** The `application.baseUrl` and the `{0}` placeholder in the value of the `baseUrl` property are required to be able to get tenant from running URL. Other placeholders in API URLs are optional. |
|||
|
|||
After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get tenant name as **mytenant1** and replace the environment object in `ConfigState` on app initialization as follows: |
|||
|
|||
|
|||
```js |
|||
// environment object in ConfigState |
|||
|
|||
{ |
|||
//... |
|||
application: { |
|||
baseUrl: 'https://mytenant1.mydomain.com/', |
|||
//... |
|||
}, |
|||
oAuthConfig: { |
|||
issuer: 'https://mytenant1.ids.mydomain.com', |
|||
//... |
|||
}, |
|||
apis: { |
|||
default: { |
|||
url: 'https://mytenant1.api.mydomain.com', |
|||
}, |
|||
AbpIdentity: { |
|||
url: 'https://mytenant1.identity.mydomain.com', |
|||
}, |
|||
}, |
|||
} |
|||
``` |
|||
|
|||
After this replacement, the app will use the following URLs: |
|||
|
|||
- `https://mytenant1.ids.mydomain.com` as IdentityServer URL. |
|||
- `https://mytenant1.api.mydomain.com` as default URL. |
|||
- `https://mytenant1.identity.mydomain.com` as `AbpIdentity` remote endpoint URL. |
|||
|
|||
The app sends the `__tenant` header that contains the current tenant id on each request. |
|||
|
|||
## See Also |
|||
|
|||
* [Multi Tenancy in ABP](../../Multi-Tenancy.md) |
|||
|
|||
## What's Next? |
|||
|
|||
- [Confirmation Popup](./Confirmation-Service.md) |
|||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1,30 @@ |
|||
# 社交/外部登录 |
|||
|
|||
## ASP.NET Core MVC / Razor Pages UI |
|||
|
|||
[帐户模块](../Modules/Account.md)已配置为开箱即用的处理社交或外部登录. 你可以按照ASP.NET Core文档向你的应用程序添加社交/外部登录提供程序. |
|||
|
|||
### 示例: Facebook 认证 |
|||
|
|||
按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. |
|||
|
|||
#### 添加NuGet包 |
|||
|
|||
添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. |
|||
|
|||
#### 配置提供程序 |
|||
|
|||
在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: |
|||
|
|||
````csharp |
|||
context.Services.AddAuthentication() |
|||
.AddFacebook(facebook => |
|||
{ |
|||
facebook.AppId = "..."; |
|||
facebook.AppSecret = "..."; |
|||
facebook.Scope.Add("email"); |
|||
facebook.Scope.Add("public_profile"); |
|||
}); |
|||
```` |
|||
|
|||
> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. |
|||
@ -0,0 +1,77 @@ |
|||
# BLOB Storing Aliyun提供程序 |
|||
|
|||
BLOB存储Aliyun提供程序可以将BLOB存储在[Aliyun Blob storage](https://help.aliyun.com/product/31815.html)中. |
|||
|
|||
> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置Aliyun提供程序. |
|||
|
|||
## 安装 |
|||
|
|||
使用ABP CLI添加[Volo.Abp.BlobStoring.Aliyun](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aliyun)NuGet包到你的项目: |
|||
|
|||
* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. |
|||
* 在要添加 `Volo.Abp.BlobStoring.Aliyun` 包的 `.csproj` 文件目录打开命令行. |
|||
* 运行 `Volo.Abp.BlobStoring.Aliyun` 命令. |
|||
|
|||
如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.Aliyun` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringAliyunModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. |
|||
|
|||
## 配置 |
|||
|
|||
如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. |
|||
|
|||
**示例: 配置为默认使用Aliyun存储提供程序** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseAliyun(aliyun => |
|||
{ |
|||
aliyun.AccessKeyId = "your aliyun access key id"; |
|||
aliyun.AccessKeySecret = "your aliyun access key secret"; |
|||
aliyun.Endpoint = "your oss endpoint"; |
|||
aliyun.RegionId = "your sts region id"; |
|||
aliyun.RoleArn = "the arn of ram role"; |
|||
aliyun.RoleSessionName = "the name of the certificate"; |
|||
aliyun.Policy = "policy"; |
|||
aliyun.DurationSeconds = "expiration date"; |
|||
aliyun.ContainerName = "your aliyun container name"; |
|||
aliyun.CreateContainerIfNotExists = false; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. |
|||
|
|||
### 选项 |
|||
|
|||
* **AccessKeyId** ([NotNull]string): 云账号AccessKey是访问阿里云API的密钥,具有该账户完全的权限,请你务必妥善保管!强烈建议遵循[阿里云安全最佳实践](https://help.aliyun.com/document_detail/102600.html),使用RAM子用户AccessKey来进行API调用. |
|||
* **AccessKeySecret** ([NotNull]string): 同上. |
|||
* **Endpoint** ([NotNull]string): Endpoint表示OSS对外服务的访问域名. [访问域名和数据中心](https://help.aliyun.com/document_detail/31837.html) |
|||
* **UseSecurityTokenService** (bool): 是否使用STS临时授权访问OSS,默认false. [STS临时授权访问OSS](https://help.aliyun.com/document_detail/100624.html) |
|||
* **RegionId** (string): STS服务的接入地址,每个地址的功能都相同,请尽量在同地域进行调用. [接入地址](https://help.aliyun.com/document_detail/66053.html) |
|||
* **RoleArn** ([NotNull]string): STS所需角色ARN. |
|||
* **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分. |
|||
* **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html). |
|||
* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. |
|||
* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): |
|||
* 只能包含小写字母,数字和短横线(-) |
|||
* 必须以小写字母和数字开头和结尾 |
|||
* Bucket名称的长度限制在**3**到**63**个字符之间 |
|||
* **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果aliyun中不存在容器, `AliyunBlobProvider` 将尝试创建它. |
|||
* **TemporaryCredentialsCacheKey** (bool): STS凭证缓存Key,默认Guid.NewGuid().ToString("N"). |
|||
|
|||
## Aliyun BLOB 名称计算器 |
|||
|
|||
Aliyun BLOB提供程序组织BLOB名称并实现一些约定. 默认情况下BLOB的全名由以下规则确定: |
|||
|
|||
* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串. |
|||
* 如果当前租户不为 `null`,则追加 `tenants/<tenant-id>` 字符串. |
|||
* 追加 BLOB 名称. |
|||
|
|||
## 其他服务 |
|||
|
|||
* `AliyunBlobProvider` 是实现Aliyun BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `AliyunBlobProvider` 类). |
|||
* `IAliyunBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultAliyunBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. |
|||
* `IOssClientFactory` 服务用于生成OSS客户端. 默认实现是 `DefaultOssClientFactory` . 如果你想自定义OSS客户端生成,可以替换/覆盖它. |
|||
@ -0,0 +1,84 @@ |
|||
# BLOB Storing Aws提供程序 |
|||
|
|||
BLOB存储Aws提供程序可以将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/cn/s3/)中. |
|||
|
|||
> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统. 本文档仅介绍如何为容器配置Aws提供程序. |
|||
|
|||
## 安装 |
|||
|
|||
使用ABP CLI添加[Volo.Abp.BlobStoring.Aws](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Aws)NuGet包到你的项目: |
|||
|
|||
* 安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), 如果你还没有安装. |
|||
* 在要添加 `Volo.Abp.BlobStoring.Aws` 包的 `.csproj` 文件目录打开命令行. |
|||
* 运行 `Volo.Abp.BlobStoring.Aws` 命令. |
|||
|
|||
如果要手动安装,在你的项目中安装 `Volo.Abp.BlobStoring.Aws` NuGet包然后将`[DependsOn(typeof(AbpBlobStoringAwsModule))]`添加到项目内的[ABP模块](Module-Development-Basics.md)类中. |
|||
|
|||
## 配置 |
|||
|
|||
如同[BLOB存储文档](Blob-Storing.md)所述,配置是在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法完成的. |
|||
|
|||
**示例: 配置为默认使用Aws存储提供程序** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseAws(Aws => |
|||
{ |
|||
Aws.AccessKeyId = "your Aws access key id"; |
|||
Aws.SecretAccessKey = "your Aws access key secret"; |
|||
Aws.UseCredentials = "set true to use credentials"; |
|||
Aws.UseTemporaryCredentials = "set true to use temporary credentials"; |
|||
Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; |
|||
Aws.ProfileName = "the name of the profile to get credentials from"; |
|||
Aws.ProfilesLocation = "the path to the aws credentials file to look at"; |
|||
Aws.Region = "the system name of the service"; |
|||
Aws.Name = "the name of the federated user"; |
|||
Aws.Policy = "policy"; |
|||
Aws.DurationSeconds = "expiration date"; |
|||
Aws.ContainerName = "your Aws container name"; |
|||
Aws.CreateContainerIfNotExists = false; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序. |
|||
|
|||
### 选项 |
|||
|
|||
* **AccessKeyId** (string): AWS Access Key ID. |
|||
* **SecretAccessKey** (string): AWS Secret Access Key. |
|||
* **UseCredentials** (bool): 使用[本地凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingAcctOrUserCredentials.html)访问AWS服务,默认: `false`. |
|||
* **UseTemporaryCredentials** (bool): 使用[临时凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingTempSessionToken.html)访问AWS服务,默认: `false`. |
|||
* **UseTemporaryFederatedCredentials** (bool): 使用[联合身份用户临时凭证](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/dev/AuthUsingTempFederationToken.html)访问AWS服务, 默认: `false`. |
|||
* **ProfileName** (string): [本地凭证配置文件](https://docs.aws.amazon.com/zh_cn/sdk-for-net/v3/developer-guide/net-dg-config-creds.html)名称. |
|||
* **ProfilesLocation** (string): 本地配置文件位置. |
|||
* **Region** (string): 服务的地区名称. |
|||
* **Policy** (string): JSON格式的IAM策略. |
|||
* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为129600. |
|||
* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): |
|||
* Bucket名称必须介于 3 到 63 个字符之间. |
|||
* Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. |
|||
* Bucket名称必须以字母或数字开头和结尾. |
|||
* Bucket名称不能是ip (例如, 192.168.5.4). |
|||
* Bucket名称不能以 **xn--** 开头, (对于2020年2月以后创建的 Bucket). |
|||
* Bucket名称在分区中必须唯一 . |
|||
* Bucket 与 Amazon S3 Transfer Acceleration 一起使用时名称中不能有句点 (.). |
|||
* **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果Aws中不存在容器, `AwsBlobProvider` 将尝试创建它. |
|||
|
|||
## Aws BLOB 名称计算器 |
|||
|
|||
Aws BLOB提供程序组织BLOB名称并实现一些约定. 默认情况下BLOB的全名由以下规则确定: |
|||
|
|||
* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串. |
|||
* 如果当前租户不为 `null`,则追加 `tenants/<tenant-id>` 字符串. |
|||
* 追加 BLOB 名称. |
|||
|
|||
## 其他服务 |
|||
|
|||
* `AwsBlobProvider` 是实现Aws BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `AwsBlobProvider` 类). |
|||
* `IAwsBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultAwsBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. |
|||
* `IAmazonS3ClientFactory` 服务用于生成AWS S3客户端. 默认实现是 `DefaultAmazonS3ClientFactory` . 如果你想自定义AWS S3客户端生成,可以替换/覆盖它. |
|||
@ -0,0 +1,69 @@ |
|||
# BLOB Storing Minio 提供程序 |
|||
|
|||
BLOB Storing Minio提供程序帮助你存储对象到 [MinIO Object storage](https://min.io/), |
|||
|
|||
> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统, 本文档仅介绍如何为容器配置Minio提供程序, |
|||
|
|||
## 安装 |
|||
|
|||
使用 ABP CLI 来安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet 包到你的项目: |
|||
|
|||
* 如果你没有安装ABP CLI,请先安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), |
|||
* 在要添加 `Volo.Abp.BlobStoring.Minio` 包的 `.csproj` 文件目录打开命令行, |
|||
* 执行 `abp add-package Volo.Abp.BlobStoring.Minio` 命令, |
|||
|
|||
如果你要手动安装, 通过NuGet安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) 到你的项目,然后添加 `[DependsOn(typeof(AbpBlobStoringMinioModule))]` 特性到你的 [ABP module](Module-Development-Basics.md) 类上, |
|||
|
|||
## 配置 |
|||
|
|||
配置在你的[module](Module-Development-Basics.md)类中的`ConfigureServices`方法中完成, |
|||
|
|||
**例: 配置使用Minio存储** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMinio(minio => |
|||
{ |
|||
minio.EndPoint = "你的 minio endPoint"; |
|||
minio.AccessKey = "你的 minio accessKey"; |
|||
minio.SecretKey = "你的 minio secretKey"; |
|||
minio.BucketName = "你的 minio bucketName"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序, |
|||
|
|||
### 选项 |
|||
|
|||
* **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档:https://docs.min.io/docs/dotnet-client-quickstart-guide.html |
|||
* **AccessKey** (string): Access key是唯一标识你的账户的用户ID, |
|||
* **SecretKey** (string): Access key是唯一标识你的账户的用户ID |
|||
* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlogContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): |
|||
* Bucket名称必须介于 3 到 63 个字符之间. |
|||
* Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. |
|||
* Bucket名称必须以字母或数字开头和结尾. |
|||
* Bucket名称不能是ip (例如, 192.168.5.4). |
|||
* Bucket名称不能以 **xn--** 开头, (对于2020年2月以后创建的 Bucket). |
|||
* Bucket名称在分区中必须唯一 . |
|||
* Bucket 与 Amazon S3 Transfer Acceleration 一起使用时名称中不能有句点 (.). |
|||
* **WithSSL** (bool): 默认 `false`,代表使用HTTPS, |
|||
* **CreateContainerIfNotExists** (bool): 默认 `false`,如果不存在bucket, `MinioBlobProvider` 将会创建一个, |
|||
|
|||
|
|||
## Minio BLOB 名称计算器 |
|||
|
|||
默认情况下BLOB的全名由以下规则确定: |
|||
|
|||
* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串, |
|||
* 如果当前租户不为 `null`,则追加 `tenants/<tenant-id>` 字符串, |
|||
* 追加 BLOB 名称, |
|||
|
|||
## 其他服务 |
|||
|
|||
* `MinioBlobProvider` 是实现Minio BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `MinioBlobProvider` 类). |
|||
* `IMinioBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultMinioBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它. |
|||
@ -0,0 +1,177 @@ |
|||
# ABP框架 v3.0 已发布 |
|||
|
|||
我们很高兴地宣布,**ABP框架**和**ABP商业版**3.0版已经发布.与常规的2周发布一个版本不同的是, 这个版本用了4周的时间.关闭了**119个[issue](https://github.com/abpframework/abp/issues?q=is%3Aopen+is%3Aissue+milestone%3A3.0)**,合并了**89个[pull request](https://github.com/abpframework/abp/pulls?q=is%3Aopen+is%3Apr+milestone%3A3.0)** 和主框架[仓库](https://github.com/abpframework/abp)中的**798次提交**. |
|||
|
|||
由于这是一个**主要版本**,它也包括了一些**重大更改**.不要害怕,这些变化都容易对应,并且下面会详细解释. |
|||
|
|||
> 详细更改日志请查看[GitHub的发布说明](https://github.com/abpframework/abp/releases/tag/3.0.0). |
|||
|
|||
## ABP框架3.0有哪些新增内容? |
|||
|
|||
这篇文章将只包括重要功能/更改.你可在[GitHub的发布说明](https://github.com/abpframework/abp/releases/tag/3.0.0)上看到所有的更改. |
|||
|
|||
### Angular 10! |
|||
|
|||
Angular 10刚刚[发布](https://blog.angular.io/version-10-of-angular-now-available-78960babd41),我们就立即迁移了[启动模板](https://docs.abp.io/en/abp/latest/Startup-Templates/Application)到Angular 10!所以,当你以Angular UI[创建一个新的解决方案](https://abp.io/get-started)时, 你将获得新Angular的优点. |
|||
|
|||
我们为想要迁移到Angular 10的老版本项目准备了一份[迁移指南](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md). |
|||
|
|||
### Oracle集成包 |
|||
|
|||
之前我们为使用Devart库, 基于EF Core for Oracle的应用程序创建了[一个集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart), 因为官方的Oracle EF Core包还未支持EF Core 3.1. 现在它以[beta发布](https://www.nuget.org/packages/Oracle.EntityFrameworkCore/3.19.0-beta1).虽然是beta版,但我们已经创建了它的[集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle),这样你就可以在你的应用程序中使用它. |
|||
|
|||
更多细节请查看[文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle). |
|||
|
|||
### Azure BLOB存储提供器 |
|||
|
|||
在上一个版本中我们创建了一个[Blob存储系统](https://docs.abp.io/en/abp/latest/Blob-Storing), 包含一个文件系统和数据库存储提供器. 本次发布我们引入了Azure BLOB存储提供器.请查看[文档](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure). |
|||
|
|||
### 分布式缓存批量操作和新的Redis缓存包 |
|||
|
|||
ASP.NET Core的[标准IDistributeCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)接口不包含**批量操作**,如使用单独方法/服务器调用设置多个项目. ABP框架引入了新的方法可用于在ABP的`IDistributedCache<T>`接口中进行批量操作: |
|||
|
|||
* GetManyAsync / GetMany |
|||
* SetManyAsync / SetMany |
|||
|
|||
然后,我们需要为Redis缓存实现这些新方法并且[必须创建](https://github.com/abpframework/abp/issues/4483)一个Redis集成包用于扩展微软的实现. |
|||
|
|||
这些方法也被ABP框架所使用, 用来缓存某个用户/角色/租户的设置,功能和权限,并且带来了**显著的性能提升**. |
|||
|
|||
更多细节请查看[缓存文档](https://docs.abp.io/en/abp/latest/Caching). |
|||
|
|||
### 为虚拟文件系统支持嵌入文件清单 |
|||
|
|||
虚拟文件系统现在支持在项目中使用`GenerateEmbeddedFilesManifest`来添加编译程序集中嵌入资源的**真实的文件/目录结构**.所以,你现在可以访问文件,而没有任何文件名的限制(以前,目录名称中的一些特殊字符,比如`.`在某些情况会有问题) |
|||
|
|||
查看[文档](https://docs.abp.io/en/abp/latest/Virtual-File-System)学习如何利用新系统的优点. |
|||
|
|||
### 新示例 |
|||
|
|||
基于社区的需求,我们准备了两个新的示例应用程序: |
|||
|
|||
* [StoredProcedureDemo](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo)演示了如何在自定义仓储库中调用存储过程,视图和函数. |
|||
* [OrganizationUnitSample](https://github.com/abpframework/abp-samples/tree/master/OrganizationUnitSample)演示了如何为你的实体使用[Identity模块](https://docs.abp.io/en/abp/latest/Modules/Identity)的组织单元系统. |
|||
|
|||
### DynamicStringLength 和 DynamicMaxLength 属性 |
|||
|
|||
当类被用作模型或[DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)时, 标准的`StringLength`和`MaxLength`数据注解属性用来验证类中的属性是很有用的. 但是, 就像任何其它的属性一样, 长度的值应该是一个**编译时**已知的字面值(常量). |
|||
|
|||
**示例: 使用`StringLength`** |
|||
|
|||
````csharp |
|||
public class CreateBookDto |
|||
{ |
|||
public const int MaxNameLength = 128; //常量! |
|||
|
|||
[StringLength(MaxNameLength)] |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
ABP框架现在有`DynamicStringLength` 和 `DynamicMaxLength`属性允许在**运行时**确定长度. |
|||
|
|||
**示例: 使用`DynamicStringLength`** |
|||
|
|||
````csharp |
|||
public class CreateBookDto |
|||
{ |
|||
public static int MaxNameLength { get; set; } = 128; |
|||
|
|||
[DynamicStringLength(typeof(CreateBookDto), nameof(MaxNameLength))] |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
`DynamicStringLength`通过类的**类型**和这个类的一个静态属性的**名字**来读取最大长度(也有一个最小长度的选项, 就像`StringLength`). |
|||
|
|||
这样就允许你从配置中读取最大值并且在应用程序启动时设置(一般在[模块](https://docs.abp.io/en/abp/latest/Module-Development-Basics)的`PreConfigureServices`方法) |
|||
|
|||
````csharp |
|||
CreateBookDto.MaxNameLength = 200; |
|||
```` |
|||
|
|||
[预建应用程序模块](https://docs.abp.io/en/abp/latest/Modules/Index)使用了这个功能,所以你现在可以覆盖这些模块中定义的属性的最大长度了. |
|||
|
|||
### 自动分布式事件 |
|||
|
|||
ABP可以为所有的实体在创建,更新和删除时**自动发布分布式事件**. 这很有用因为在分布式系统中你通常会对这些基础事件感兴趣. |
|||
|
|||
这个功能在v3.0中**已成熟并[文档化了](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus#pre-defined-events)**. 你可以轻松地配置一部分或全部要发布事件的实体. |
|||
|
|||
### IAsyncQueryableExecuter |
|||
|
|||
使用LINQ扩展方法,当你在查询时需要调用`ToListAsync()`,`FirstOrDefaultAsync()`... 不幸的是,这些方法并不是**标准的**LINQ扩展方法.它们定义在[Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)包中(或在[MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver/),如果你使用MongoDB的话). |
|||
|
|||
所以,如果你想使用异步扩展方法, 你需要依赖这个包.这样做破坏了分层,并且使你的应用程序或领域层依赖于EF Core / MongoDB的包. |
|||
|
|||
`IAsyncQueryableExecuter`是ABP框架定义的服务, 用来**不依赖特定提供器地执行异步查询**(EF Core / MongoDB包). |
|||
|
|||
查看[文档](https://docs.abp.io/en/abp/latest/Repositories#iqueryable-async-operations)来获取细节和了解我们的建议. |
|||
|
|||
### API 文档 |
|||
|
|||
现在我们为每次发布ABP框架和模块都公开[API文档](https://docs.abp.io/api-docs/abp/3.0/api/index.html).所以,你可以更容易地查看ABP框架的类.点击[文档](https://docs.abp.io/en/abp/latest/)导航菜单中的**API文档**的链接. |
|||
|
|||
### 包列表 |
|||
|
|||
我们[创建了一个页面](http://abp.io/packages)列出所有ABP相关的官方NuGet和NPM包. |
|||
|
|||
### 其他 |
|||
|
|||
* 为Identity Server实现了前端通道的服务端客户[登出](https://identityserver4.readthedocs.io/en/latest/topics/signout.html) |
|||
* `abp.currentUser`(Angular UI中的`CurrentUser`服务)现在有一个包含当前用户的角色名的`roles`数组. |
|||
* 升级所有的NuGet和NPM软件包的依赖. |
|||
* 引入了`ReadOnlyAppService`基类(只包含get操作), 做为`CrudAppService`基类(其中有get,create,update和delete操作)的补充. |
|||
|
|||
其他更新请查看[GitHub发行说明](https://github.com/abpframework/abp/releases/tag/3.0.0). |
|||
|
|||
## ABP商业版3.0有哪些新增内容? |
|||
|
|||
除了所有ABP框架的功能以外,ABP商业版同样在本次发布中有一些额外的功能.本节介绍3.0版本中[ABP商业版](https://commercial.abp.io/)的亮点. |
|||
|
|||
### 新的文件管理模块 |
|||
|
|||
我们创建了一个用于在应用程序中存储和管理文件的新模块.这个新模块基于[BLOB存储系统](https://docs.abp.io/en/abp/latest/Blob-Storing),因此它可以使用不同的存储提供器来存储文件内容. |
|||
|
|||
**示例截图** |
|||
|
|||
 |
|||
|
|||
您可以上传,下载和以分层文件夹结构组织文件.它也兼容多租户,你可以为租户限制总文件大小.在未来的版本中,我们将致力于“共享”系统, 用于以一个更可控的方式在用户之间或通过公开链接与你的客户共享文件. |
|||
|
|||
> 文件管理模块是目前仅适用于MVC / Razor Pages UI.我们正工作于Angular UI,它会在未来的版本中发布. |
|||
|
|||
## 重大更改 |
|||
|
|||
由于这是一个主要版本,我们重新设计了一些API,并引入了一些"容易解决"的重大更改. |
|||
|
|||
### ABP框架 |
|||
|
|||
* 更改了一些预建应用程序模块的**常量**为静态属性, 这样你就可在你的代码中对它们修改. 如果你在属性中使用了这些常量,那么请使用上面介绍的`DynamicStringLength`. |
|||
* 更改`ConcurrencyStamp`最大长度为40. 你需要在更新ABP框架后**添加一个数据库迁移**并且更新数据库. |
|||
* NPM软件包的依赖关系使用`~`代替`^`,为了更加稳定. |
|||
|
|||
### ABP商业版 |
|||
|
|||
* 更改了应用程序标识的文件名.此前,它为每个主题使用了单独的标识文件,如`theme1.png`,`theme1-reverse.png`,`theme2.png`,`theme2-reverse.png`(...`6`).现在,我们只有两个标识文件:`logo-light.png`和`logo-dark.png`.所以,重命名MVC UI的`wwwroot/images/logo/`和Angular UI的`/src/assets/images/logo/`下的标识. |
|||
* 我们也增加了ABP商业版的[API文档](https://docs.abp.io/api-docs/commercial/3.0/api/index.html). |
|||
|
|||
> **此外,Angular UI请查看[迁移指南](https://github.com/abpframework/abp/blob/dev/docs/en/UI/Angular/Migration-Guide-v3.md)**. |
|||
|
|||
## 已知问题 |
|||
|
|||
* 3.0.0版本有一个分层架构的问题.查看[这个issue](https://github.com/abpframework/abp/pull/4564)为你的应用程序解决它,直到我们发布V3.0.1. |
|||
|
|||
## 关于未来的版本 |
|||
|
|||
我们将继续在每两个星期发布一个新的小功能版本.所以,下一个预计发布日期是**2020年7月16日**的**3.1**版本. |
|||
|
|||
在接下来的几个版本中,我们将专注于**Blazor UI**,如[路线图](https://docs.abp.io/en/abp/latest/Road-Map)中承诺的.我们将继续改进文档,创建示例,添加其他新功能和改善.关注[ABP框架Twitter账号](https://twitter.com/abpframework)获取最新消息... |
|||
|
|||
## 彩蛋: 文章! |
|||
|
|||
除了开发我们的产品,我们的团队一直在不断书写各种主题的文章/教程.你可能感兴趣的最新文章: |
|||
|
|||
* [Angular 10有哪些新增内容?](https://volosoft.com/blog/what-is-new-in-angular-10) |
|||
* [在分布式架构中使用ABP, SignalR和RabbitMQ构建实时消息系统](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) |
|||
* [如何在Angular模板中使用Attribute Directives来避免重复](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) |
|||
|
After Width: | Height: | Size: 116 KiB |
@ -1,124 +1,3 @@ |
|||
## 在控制台应用中使用ABP |
|||
|
|||
本教程将介绍如何从头开始以最小的依赖关系启动ABP. 你通常希望以 **[启动模板](https://abp.io/Templates)** 开头. |
|||
|
|||
### 创建一个新项目 |
|||
|
|||
使用Visual Studio创建一个新的.Net Core Console应用程序: |
|||
|
|||
 |
|||
|
|||
### 安装 Volo.Abp 包 |
|||
|
|||
Volo.Abp.Core是创建基于ABP的应用程序的核心nuget包. 所以,将它安装到你的项目中: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Core |
|||
```` |
|||
|
|||
### 创建第一个ABP模块 |
|||
|
|||
ABP是一个模块化框架, 它需要一个从``AbpModule``类派生的 **启动(根)模块** 类: |
|||
|
|||
````C# |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class AppModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
``AppModule`` 是应用程序启动模块的好名称. |
|||
|
|||
### 初始化应用程序 |
|||
|
|||
下一步是使用上面创建的启动模块引导应用程序: |
|||
|
|||
````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`` 用于创建应用程序并加载所有以``AppModule``作为启动模块的模块. ``Initialize()``方法启动应用程序. |
|||
|
|||
### Hello World! |
|||
|
|||
上面的应用程序什么都不做, 让我们创建一个服务做一些事情: |
|||
|
|||
````C# |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpConsoleDemo |
|||
{ |
|||
public class HelloWorldService : ITransientDependency |
|||
{ |
|||
public void SayHello() |
|||
{ |
|||
Console.WriteLine("Hello World!"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
``ITransientDependency``是ABP的一个特殊接口, 它自动将服务注册为**Transient**(参见[依赖注入文档](Dependency-Injection.md)). |
|||
|
|||
现在,我们可以解析``HelloWorldService``并调用``SayHello``. 更改Program.cs, 如下所示: |
|||
|
|||
````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(); |
|||
|
|||
// 解析服务并使用它 |
|||
var helloWorldService = |
|||
application.ServiceProvider.GetService<HelloWorldService>(); |
|||
helloWorldService.SayHello(); |
|||
|
|||
Console.WriteLine("Press ENTER to stop application..."); |
|||
Console.ReadLine(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这对于这个简单的代码示例已足够, 如果是直接从``IServiceProvider``解析建议创建**Scoped**依赖.(参见[依赖注入文档](Dependency-Injection.md)). |
|||
|
|||
### 源码 |
|||
|
|||
从[这里](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication)获取本教程中创建的示例项目的源代码. |
|||
ABP提供了控制台应用程序启动模板. 参阅[控制台应用程序启动模板]文档了解更多信息. |
|||
@ -0,0 +1,70 @@ |
|||
# React Native入门 |
|||
|
|||
ABP平台提供了[React Native](https://reactnative.dev/)模板用于开发移动应用程序. |
|||
|
|||
当你按照[入门文档](Getting-Started.md)中所述**创建新应用程序**时,解决方案默认将React Native应用程序包含在 `react-native` 文件夹中. |
|||
|
|||
## 配置你的本地IP地址 |
|||
|
|||
运行在Android模拟器或真机上的React Native应用程序无法连接到 `localhost` 上的后.要修复此问题,需要在本地IP上运行后端. |
|||
|
|||
{{ if Tiered == "No"}} |
|||
 |
|||
|
|||
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 和 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
|
|||
{{ else if Tiered == "Yes" }} |
|||
|
|||
 |
|||
|
|||
* 打开 `.IdentityServer` 文件夹下的 `appsettings.json` 文件. 将 `SelfUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.IdentityServer/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host` 文件夹下的 `appsettings.json` 文件. 将 `Authority` 属性的 `localhost` 替换为你本地的IP地址. |
|||
* 打开 `.HttpApi.Host/Properties` 文件夹下的 `launchSettings.json` 文件. 将 `applicationUrl` 属性的 `localhost` 替换为你本地的IP地址. |
|||
|
|||
{{ end }} |
|||
|
|||
按照**运行HTTP API Host (服务端口)**那样运行后端. |
|||
|
|||
> React Native应用程序不信任自动生成的.NET HTTPS证书,你可以在开发期间使用HTTP. |
|||
|
|||
在 `react-native` 文件夹打开命令行终端,输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作). |
|||
|
|||
```bash |
|||
yarn |
|||
``` |
|||
|
|||
* 打开 `react-nativer` 文件夹下的 `Environment.js` 文件. 将 `apiUrl` 和 `issuer` 属性的 `localhost` 替换为你本地的IP地址: |
|||
|
|||
 |
|||
|
|||
{{ if Tiered == "Yes" }} |
|||
|
|||
> 确保 `issuer` 与正在运行的 `.IdentityServer` 项目匹配, `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配. |
|||
|
|||
{{else}} |
|||
|
|||
> 确保 `issuer` 和 `apiUrl` 与正在运行的 `.HttpApi.Host` 项目匹配 |
|||
|
|||
{{ end }} |
|||
|
|||
等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令: |
|||
|
|||
```bash |
|||
yarn start |
|||
``` |
|||
|
|||
等待Expo CLI启动后Expo CLI在 `http://localhost:19002/` 地址要开管理页面. |
|||
|
|||
 |
|||
|
|||
在上面的管理界面中,可以通过使用[Expo Client](https://expo.io/tools#client)扫描二维码,使用Android模拟器,iOS模拟器或真机来启动应用程序. |
|||
|
|||
> 请参阅expo.io上的[Android Studio模拟器](https://docs.expo.io/workflow/android-simulator/)和[iOS模拟器文档](https://docs.expo.io/workflow/ios-simulator/). |
|||
|
|||
 |
|||
|
|||
输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序. |
|||
|
|||
应用程序已经启动并执行,你可以基于该启动模板开发应用程序. |
|||