Browse Source

Merge 2.6.2

pull/3321/head
Luis Pignataro 6 years ago
parent
commit
84bc8e7cb9
  1. 30
      .github/workflows/main.yml
  2. 13
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json
  3. 37
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  4. 90
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ru.json
  5. 8
      build/common.ps1
  6. 7
      common.DotSettings
  7. 2
      common.props
  8. 3
      docs/cs/AspNetCore/Widgets.md
  9. 4
      docs/cs/Contribution/Localization-Text-Files.md
  10. 2
      docs/cs/Getting-Started-AspNetCore-Application.md
  11. 2
      docs/cs/Getting-Started-Console-Application.md
  12. 4
      docs/en/API/Dynamic-CSharp-API-Clients.md
  13. 51
      docs/en/Application-Services.md
  14. 30
      docs/en/Authorization.md
  15. 3
      docs/en/AutoMapper-Integration.md
  16. 43
      docs/en/Background-Jobs-Hangfire.md
  17. 26
      docs/en/Best-Practices/Application-Services.md
  18. 1
      docs/en/Best-Practices/Data-Transfer-Objects.md
  19. 9
      docs/en/Best-Practices/Entity-Framework-Core-Integration.md
  20. 4
      docs/en/Best-Practices/MongoDB-Integration.md
  21. 10
      docs/en/CLI.md
  22. 4
      docs/en/Contribution/Localization-Text-Files.md
  23. 38
      docs/en/Customizing-Application-Modules-Extending-Entities.md
  24. 102
      docs/en/Customizing-Application-Modules-Overriding-Services.md
  25. 11
      docs/en/Entities.md
  26. 109
      docs/en/Entity-Framework-Core-Migrations.md
  27. 112
      docs/en/Entity-Framework-Core.md
  28. 52
      docs/en/Exception-Handling.md
  29. 128
      docs/en/Getting-Started-Angular-Template.md
  30. 2
      docs/en/Getting-Started-AspNetCore-Application.md
  31. 106
      docs/en/Getting-Started-AspNetCore-MVC-Template.md
  32. 2
      docs/en/Getting-Started-Console-Application.md
  33. 8
      docs/en/Getting-Started-With-Startup-Templates.md
  34. 407
      docs/en/Getting-Started.md
  35. 201
      docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
  36. 113
      docs/en/How-To/Customize-Login-Page-MVC.md
  37. 101
      docs/en/How-To/Customize-SignIn-Manager.md
  38. 9
      docs/en/How-To/Index.md
  39. 2
      docs/en/Modules/Docs.md
  40. 8
      docs/en/Multi-Tenancy.md
  41. 15
      docs/en/Nightly-Builds.md
  42. 365
      docs/en/Object-Extensions.md
  43. 17
      docs/en/Object-To-Object-Mapping.md
  44. 192
      docs/en/Startup-Templates/Application.md
  45. 2
      docs/en/Tutorials/Angular/Part-I.md
  46. 2
      docs/en/Tutorials/Angular/Part-II.md
  47. 2
      docs/en/Tutorials/Angular/Part-III.md
  48. 2
      docs/en/Tutorials/AspNetCore-Mvc/Part-I.md
  49. 2
      docs/en/Tutorials/AspNetCore-Mvc/Part-II.md
  50. 2
      docs/en/Tutorials/AspNetCore-Mvc/Part-III.md
  51. 166
      docs/en/Tutorials/Part-1.md
  52. 533
      docs/en/Tutorials/Part-2.md
  53. 4
      docs/en/Tutorials/Part-3.md
  54. BIN
      docs/en/Tutorials/images/bookstore-angular-file-tree.png
  55. BIN
      docs/en/Tutorials/images/generate-proxy-command.png
  56. BIN
      docs/en/Tutorials/images/generated-proxies.png
  57. 77
      docs/en/UI/Angular/Component-Replacement.md
  58. 14
      docs/en/UI/Angular/Config-State.md
  59. 185
      docs/en/UI/Angular/Confirmation-Service.md
  60. 101
      docs/en/UI/Angular/Container-Strategy.md
  61. 78
      docs/en/UI/Angular/Content-Projection-Service.md
  62. 74
      docs/en/UI/Angular/Content-Security-Strategy.md
  63. 95
      docs/en/UI/Angular/Content-Strategy.md
  64. 117
      docs/en/UI/Angular/Context-Strategy.md
  65. 60
      docs/en/UI/Angular/Cross-Origin-Strategy.md
  66. 4
      docs/en/UI/Angular/Custom-Setting-Page.md
  67. 140
      docs/en/UI/Angular/Dom-Insertion-Service.md
  68. 90
      docs/en/UI/Angular/Dom-Strategy.md
  69. 209
      docs/en/UI/Angular/HTTP-Requests.md
  70. 213
      docs/en/UI/Angular/Lazy-Load-Service.md
  71. 110
      docs/en/UI/Angular/Loading-Strategy.md
  72. 6
      docs/en/UI/Angular/Localization.md
  73. 199
      docs/en/UI/Angular/Modifying-the-Menu.md
  74. 2
      docs/en/UI/Angular/Permission-Management.md
  75. 200
      docs/en/UI/Angular/Projection-Strategy.md
  76. 67
      docs/en/UI/Angular/Service-Proxies.md
  77. 158
      docs/en/UI/Angular/Toaster-Service.md
  78. 113
      docs/en/UI/Angular/Track-By-Service.md
  79. BIN
      docs/en/UI/Angular/images/confirmation.png
  80. BIN
      docs/en/UI/Angular/images/generated-files-via-generate-proxy.png
  81. BIN
      docs/en/UI/Angular/images/navigation-menu-after-patching.png
  82. BIN
      docs/en/UI/Angular/images/navigation-menu-search-input.png
  83. BIN
      docs/en/UI/Angular/images/navigation-menu-via-app-routing.png
  84. BIN
      docs/en/UI/Angular/images/navigation-menu-via-config-state.png
  85. BIN
      docs/en/UI/Angular/images/toast.png
  86. 4
      docs/en/UI/AspNetCore/Bundling-Minification.md
  87. 278
      docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md
  88. 261
      docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md
  89. 14
      docs/en/UI/AspNetCore/Tag-Helpers/Index.md
  90. 2
      docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md
  91. 42
      docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md
  92. 4
      docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md
  93. 2
      docs/en/UI/AspNetCore/Tag-Helpers/Tabs.md
  94. 2
      docs/en/UI/AspNetCore/Widgets.md
  95. 1616
      docs/en/UI/Common/Utils/Linked-List.md
  96. 96
      docs/en/docs-nav.json
  97. BIN
      docs/en/images/angular-folder-structure.png
  98. BIN
      docs/en/images/angular-template-structure-diagram.png
  99. 0
      docs/en/images/bookstore-home.png
  100. BIN
      docs/en/images/bookstore-login.png

30
.github/workflows/main.yml

@ -0,0 +1,30 @@
name: "Main"
on:
pull_request:
paths:
- "framework/**"
- "modules/**"
- "templates/**"
push:
paths:
- "framework/**"
- "modules/**"
- "templates/**"
jobs:
build-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@master
with:
dotnet-version: 3.1.100
- name: Build All
run: .\build-all.ps1
working-directory: .\build
shell: powershell
- name: Test All
run: .\test-all.ps1
working-directory: .\build
shell: powershell

13
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json

@ -0,0 +1,13 @@
{
"culture": "ru",
"texts": {
"Account": "Аккаунт",
"Welcome": "Добро пожаловать",
"UseOneOfTheFollowingLinksToContinue": "Для продолжения используйте одну из следующих ссылок",
"FrameworkHomePage": "Главная страница фреймворка",
"FrameworkDocumentation": "Документация фреймворка",
"OfficialBlog": "Официальный блог",
"CommercialHomePage": "Главная страница коммерческой версии",
"CommercialSupportWebSite": "Сайт коммерческой поддержки"
}
}

37
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -13,7 +13,11 @@
"Permission:Edit": "Edit", "Permission:Edit": "Edit",
"Permission:Delete": "Delete", "Permission:Delete": "Delete",
"Permission:Create": "Create", "Permission:Create": "Create",
"Permission:Accounting": "Accounting",
"Permission:Accounting:Quotation": "Quotation",
"Permission:Accounting:Invoice": "Invoice",
"Menu:Organizations": "Organizations", "Menu:Organizations": "Organizations",
"Menu:Accounting": "Accounting",
"Menu:Packages": "Packages", "Menu:Packages": "Packages",
"NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?", "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?",
"NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?", "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?",
@ -75,7 +79,8 @@
"AddDeveloper": "Add developer", "AddDeveloper": "Add developer",
"Create": "Create", "Create": "Create",
"UserNotFound": "User not found", "UserNotFound": "User not found",
"{0}WillBeRemovedFromMembers": "{0} Will be removed from members", "{0}WillBeRemovedFromDevelopers": "{0} Will be removed from developers, do you confirm?",
"{0}WillBeRemovedFromOwners": "{0} Will be removed from owners, do you confirm?",
"Computers": "Computers", "Computers": "Computers",
"UniqueComputerId": "Unique computer id", "UniqueComputerId": "Unique computer id",
"LastSeenDate": "Last seen date", "LastSeenDate": "Last seen date",
@ -86,6 +91,34 @@
"AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?", "AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?",
"DeleteAll": "Delete all", "DeleteAll": "Delete all",
"DoYouWantToCreateNewUser": "Do you want to create new user?", "DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules" "MasterModules": "Master Modules",
"OrganizationName": "Organization name",
"OrganizationNamePlaceholder": "Organization name...",
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
"PurchaseOrderNo": "Purchase order no",
"QuotationDate": "Quotation date",
"CompanyName": "Company name",
"CompanyAddress": "Company address",
"Price": "Price",
"DiscountText": "Discount text",
"DiscountQuantity": "Discount quantity",
"DiscountPrice": "Discount price",
"Quotation": "Quotation",
"ExtraText": "Extra Text",
"ExtraAmount": "Extra Amount",
"DownloadQuotation": "Download Quotation",
"Invoice": "Invoice",
"TaxNumber": "Tax Number",
"InvoiceNumber": "Invoice Number",
"InvoiceDate": "Invoice Date",
"Quantity": "Quantity",
"AddProduct": "Add Product",
"AddProductWarning": "You need to add product!",
"TotalPrice": "Total Price",
"Generate": "Generate",
"MissingQuantityField": "The quantity field is required!",
"MissingPriceField": "The Price field is required!"
} }
} }

90
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/ru.json

@ -0,0 +1,90 @@
{
"culture": "ru",
"texts": {
"Permission:Organizations": "Организации",
"Permission:Manage": "Управление организациями",
"Permission:NpmPackages": "Пакеты NPM",
"Permission:NugetPackages": "Пакеты NuGet",
"Permission:Maintenance": "Обслуживание",
"Permission:Maintain": "Обслуживать",
"Permission:ClearCaches": "Очистить кэш",
"Permission:Modules": "Модули",
"Permission:Packages": "Пакеты",
"Permission:Edit": "Редактировать",
"Permission:Delete": "Удалить",
"Permission:Create": "Создать",
"Menu:Organizations": "Организации",
"Menu:Packages": "Пакеты",
"NpmPackageDeletionWarningMessage": "Этот пакет NPM будет удален. Вы подтверждаете это?",
"NugetPackageDeletionWarningMessage": "Этот пакет NuGet будет удален. Вы подтверждаете это?",
"ModuleDeletionWarningMessage": "Этот модуль будет удален. Вы подтверждаете это?",
"Name": "Имя",
"DisplayName": "Отображаемое имя",
"ShortDescription": "Краткое описание",
"NameFilter": "Имя",
"CreationTime": "Время создания",
"IsPro": "Is pro",
"EfCoreConfigureMethodName": "Настроить имя метода",
"IsProFilter": "Is pro",
"ApplicationType": "Тип приложения",
"Target": "Цель",
"TargetFilter": "Цель",
"ModuleClass": "Класс модуля",
"NugetPackageTarget.DomainShared": "Domain Shared",
"NugetPackageTarget.Domain": "Domain",
"NugetPackageTarget.Application": "Application",
"NugetPackageTarget.ApplicationContracts": "Application Contracts",
"NugetPackageTarget.HttpApi": "Http Api",
"NugetPackageTarget.HttpApiClient": "Http Api Client",
"NugetPackageTarget.Web": "Web",
"NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core",
"NugetPackageTarget.MongoDB": "MongoDB",
"Edit": "Редактировать",
"Delete": "Удалить",
"Refresh": "Обновить",
"NpmPackages": "NPM пакеты",
"NugetPackages": "NuGet пакеты",
"NpmPackageCount": "Количество пакетов NPM",
"NugetPackageCount": "Количество пакетов NuGet",
"Module": "Модули",
"ModuleInfo": "Информация о модуле",
"CreateANpmPackage": "Создать пакет NPM",
"CreateAModule": "Создать модуль",
"CreateANugetPackage": "Создать пакет NuGet",
"AddNew": "Добавить новый",
"PackageAlreadyExist{0}": "\"{0}\" пакет уже существует.",
"ModuleAlreadyExist{0}": "\"{0}\" модуль уже добавлен.",
"ClearCache": "Очистить кэш",
"SuccessfullyCleared": "Успешно очищено",
"Menu:NpmPackages": "Пакеты NPM",
"Menu:Modules": "Модули",
"Menu:Maintenance": "Поддержка",
"Menu:NugetPackages": "Пакеты NuGet",
"CreateAnOrganization": "Создать организацию",
"Organizations": "Организации",
"LongName": "Полное название",
"LicenseType": "Тип лицензии",
"LicenseStartTime": "Время начала действия лицензии",
"LicenseEndTime": "Время окончания действия лицензии",
"AllowedDeveloperCount": "Разрешенное количество разработчиков",
"UserNameOrEmailAddress": "Имя пользователя или адрес электронной почты",
"AddOwner": "Добавить владельца",
"UserName": "Имя пользователя",
"Email": "Электронная почта",
"Developers": "Разработчики",
"AddDeveloper": "Добавить разработчика",
"Create": "Создать",
"UserNotFound": "Пользователь не обнаружен",
"{0}WillBeRemovedFromMembers": "{0} будет удален из членов",
"Computers": "Компьютеры",
"UniqueComputerId": "Уникальный id компьютера",
"LastSeenDate": "Дата последнего визита",
"{0}Computer{1}WillBeRemovedFromRecords": "Компьютер {0} ({1}) будет удален из записей",
"OrganizationDeletionWarningMessage": "Организация будет удалена",
"This{0}AlreadyExistInThisOrganization": "{0} уже существует в данной организации",
"AreYouSureYouWantToDeleteAllComputers": "Вы уверены, что хотите удалить все компьютеры?",
"DeleteAll": "Удалить все",
"DoYouWantToCreateNewUser": "Вы хотите создать нового пользователя?",
"MasterModules": "Мастер модулей"
}
}

8
build/common.ps1

@ -21,14 +21,6 @@ $solutionPaths = (
"../modules/client-simulation", "../modules/client-simulation",
"../templates/module/aspnet-core", "../templates/module/aspnet-core",
"../templates/app/aspnet-core", "../templates/app/aspnet-core",
"../samples/BasicAspNetCoreApplication",
"../samples/BasicConsoleApplication",
"../samples/BookStore",
"../samples/BookStore-Angular-MongoDb/aspnet-core",
"../samples/BookStore-Modular/modules/book-management",
"../samples/BookStore-Modular/application",
"../samples/DashboardDemo",
"../samples/MicroserviceDemo", "../samples/MicroserviceDemo",
"../samples/RabbitMqEventBus",
"../abp_io/AbpIoLocalization" "../abp_io/AbpIoLocalization"
) )

7
common.DotSettings

@ -20,4 +20,11 @@
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Async/@EntryIndexedValue">False</s:String> <s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Async/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String> <s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/HideTypeNameHintsWhenTypeNameIsEvidentFromVariableName/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowMethodReturnTypeNameHints/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForImplicitlyTypedVariables/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForLambdaExpressionParameters/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForLinqQueryRangeVariables/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForPatternMatchingExpressions/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Volo/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary> </wpf:ResourceDictionary>

2
common.props

@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Version>2.4.0</Version> <Version>2.7.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl> <PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io</PackageProjectUrl> <PackageProjectUrl>https://abp.io</PackageProjectUrl>

3
docs/cs/AspNetCore/Widgets.md

@ -502,4 +502,5 @@ Configure<AbpWidgetOptions>(options =>
## Podívejte se také na ## Podívejte se také na
* [Příklad projektu (zdrojový kód)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). * [Příklad projektu (zdrojový kód)](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo).

4
docs/cs/Contribution/Localization-Text-Files.md

@ -29,8 +29,8 @@ Toto je seznam lokalizačních textových souborů pro každého kdo chce přisp
* https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Localization/Resources/AbpSettingManagement/en.json * https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Localization/Resources/AbpSettingManagement/en.json
* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/Localization/ApplicationContracts/en.json * https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/Localization/ApplicationContracts/en.json
* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Localization/Resources/AbpTenantManagement/Web/en.json * https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Localization/Resources/AbpTenantManagement/Web/en.json
* https://github.com/abpframework/abp/tree/master/samples/BookStore/src/Acme.BookStore.Domain.Shared/Localization/BookStore/en.json * https://github.com/abpframework/abp-samples/tree/master/BookStore/src/Acme.BookStore.Domain.Shared/Localization/BookStore/en.json
* https://github.com/abpframework/abp/tree/master/samples/DashboardDemo/src/DashboardDemo.Domain/Localization/DashboardDemo/en.json * https://github.com/abpframework/abp-samples/tree/master/DashboardDemo/src/DashboardDemo.Domain/Localization/DashboardDemo/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application.Contracts/ProductManagement/Localization/ApplicationContracts/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application.Contracts/ProductManagement/Localization/ApplicationContracts/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Localization/Domain/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Localization/Domain/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Web/Localization/Resources/ProductManagement/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Web/Localization/Resources/ProductManagement/en.json

2
docs/cs/Getting-Started-AspNetCore-Application.md

@ -153,5 +153,5 @@ namespace BasicAspNetCoreApplication
## Zdrojový kód ## Zdrojový kód
Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication). Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication).

2
docs/cs/Getting-Started-Console-Application.md

@ -178,4 +178,4 @@ Stačí volat metodu `options.UseAutofac()` v možnostech `AbpApplicationFactory
## Zdrojový kód ## Zdrojový kód
Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp/tree/master/samples/BasicConsoleApplication). Získejte zdrojový kód vzorového projektu vytvořeného v tomto tutoriálů [z tohoto odkazu](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication).

4
docs/en/API/Dynamic-CSharp-API-Clients.md

@ -69,7 +69,7 @@ public class MyClientAppModule : AbpModule
} }
```` ````
See the "RemoteServiceOptions" section below for more detailed configuration. See the "AbpRemoteServiceOptions" section below for more detailed configuration.
## Usage ## Usage
@ -104,7 +104,7 @@ While you can inject `IBookAppService` like above to use the client proxy, you c
## Configuration ## Configuration
### RemoteServiceOptions ### AbpRemoteServiceOptions
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: `AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example:

51
docs/en/Application-Services.md

@ -132,6 +132,8 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea
However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier. However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier.
Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default.
Let's create another method to get a book. First, define the method in the `IBookAppService` interface: Let's create another method to get a book. First, define the method in the `IBookAppService` interface:
````csharp ````csharp
@ -146,7 +148,6 @@ public interface IBookAppService : IApplicationService
`BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below: `BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below:
````csharp ````csharp
[AbpAutoMapFrom(typeof(Book))] //Defines the mapping
public class BookDto public class BookDto
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
@ -159,21 +160,38 @@ public class BookDto
} }
```` ````
* `BookDto` defines `[AbpAutoMapFrom(typeof(Book))]` attribute to create the object mapping from `Book` to `BookDto`. AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example:
Then you can implement the `GetAsync` method as shown below: ````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
}
}
````
You should then register profiles using the `AbpAutoMapperOptions`:
````csharp ````csharp
public async Task<BookDto> GetAsync(Guid id) [DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{ {
var book = await _bookRepository.GetAsync(id); public override void ConfigureServices(ServiceConfigurationContext context)
return book.MapTo<BookDto>(); {
Configure<AbpAutoMapperOptions>(options =>
{
//Add all mappings defined in the assembly of the MyModule class
options.AddMaps<MyModule>();
});
}
} }
```` ````
`MapTo` extension method converts `Book` object to `BookDto` object by copying all properties with the same naming. `AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html).
An alternative to the `MapTo` is using the `IObjectMapper` service: Then you can implement the `GetAsync` method as shown below:
````csharp ````csharp
public async Task<BookDto> GetAsync(Guid id) public async Task<BookDto> GetAsync(Guid id)
@ -183,8 +201,6 @@ public async Task<BookDto> GetAsync(Guid id)
} }
```` ````
While the second syntax is a bit harder to write, it better works if you write unit tests.
See the [object to object mapping document](Object-To-Object-Mapping.md) for more. See the [object to object mapping document](Object-To-Object-Mapping.md) for more.
## Validation ## Validation
@ -250,7 +266,6 @@ public interface ICrudAppService<
DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`: DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`:
````csharp ````csharp
[AbpAutoMapFrom(typeof(Book))]
public class BookDto : AuditedEntityDto<Guid> public class BookDto : AuditedEntityDto<Guid>
{ {
public string Name { get; set; } public string Name { get; set; }
@ -260,7 +275,6 @@ public class BookDto : AuditedEntityDto<Guid>
public float Price { get; set; } public float Price { get; set; }
} }
[AbpAutoMapTo(typeof(Book))]
public class CreateUpdateBookDto public class CreateUpdateBookDto
{ {
[Required] [Required]
@ -275,6 +289,19 @@ public class CreateUpdateBookDto
} }
```` ````
[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class.
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
```
* `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well. * `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well.
And finally, the `BookAppService` implementation is very simple: And finally, the `BookAppService` implementation is very simple:

30
docs/en/Authorization.md

@ -142,6 +142,20 @@ myGroup.AddPermission(
); );
``` ```
#### Enable/Disable Permissions
A permission is enabled by default. It is possible to disable a permission. A disabled permission will be prohibited for everyone. You can still check for the permission, but it will always return prohibited.
Example definition:
````csharp
myGroup.AddPermission("Author_Management", isEnabled: false);
````
You normally don't need to define a disabled permission (unless you temporary want disable a feature of your application). However, you may want to disable a permission defined in a depended module. In this way you can disable the related application functionality. See the "*Changing Permission Definitions of a Depended Module*" section below for an example usage.
> Note: Checking an undefined permission will throw an exception while a disabled permission check simply returns prohibited (false).
#### Child Permissions #### Child Permissions
A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted. A permission may have child permissions. It is especially useful when you want to create a hierarchical permission tree where a permission may have additional sub permissions which are available only if the parent permission has been granted.
@ -208,6 +222,18 @@ See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/se
A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
Example:
````csharp
context
.GetPermissionOrNull(IdentityPermissions.Roles.Delete)
.IsEnabled = false;
````
When you write this code inside your permission definition provider, it finds the "role deletion" permission of the [Identity Module](Modules/Identity.md) and disabled the permission, so no one can delete a role on the application.
> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined.
## IAuthorizationService ## IAuthorizationService
ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization. ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization.
@ -339,10 +365,10 @@ A permission value provider should return one of the following values from the `
- `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return. - `PermissionGrantResult.Prohibited` is returned to prohibit the user for the permission. If any of the providers return `Prohibited`, the result will always be `Prohibited`. Doesn't matter what other providers return.
- `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission. - `PermissionGrantResult.Undefined` is returned if this value provider could not decide about the permission value. Return this to let other providers check the permission.
Once a provider is defined, it should be added to the `PermissionOptions` as shown below: Once a provider is defined, it should be added to the `AbpPermissionOptions` as shown below:
```csharp ```csharp
Configure<PermissionOptions>(options => Configure<AbpPermissionOptions>(options =>
{ {
options.ValueProviders.Add<SystemAdminPermissionValueProvider>(); options.ValueProviders.Add<SystemAdminPermissionValueProvider>();
}); });

3
docs/en/AutoMapper-Integration.md

@ -1,3 +0,0 @@
## AutoMapper Integration
TODO

43
docs/en/Background-Jobs-Hangfire.md

@ -40,4 +40,45 @@ public class YourModule : AbpModule
## Configuration ## Configuration
TODO... You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package).
After you have installed these NuGet packages, you need to configure your project to use Hangfire.
1.First, we change the `Module` class (example: `<YourProjectName>HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
//... other configarations.
ConfigureHangfire(context, configuration);
}
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddHangfire(config =>
{
config.UseSqlServerStorage(configuration.GetConnectionString("Default"));
});
}
````
2. We need to add `UseHangfireServer` call in the `OnApplicationInitialization` method in `Module` class
If you want to use hangfire's dashboard, you can add it, too: by `UseHangfireDashboard`
````csharp
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
// ... others
app.UseHangfireServer();
app.UseHangfireDashboard();
}
````

26
docs/en/Best-Practices/Application-Services.md

@ -17,17 +17,18 @@
##### Basic DTO ##### Basic DTO
**Do** define a **basic** DTO for an entity. **Do** define a **basic** DTO for an aggregate root.
- Include all the **primitive properties** directly on the entity. - Include all the **primitive properties** directly on the aggregate root.
- Exception: Can **exclude** properties for **security** reasons (like User.Password). - Exception: Can **exclude** properties for **security** reasons (like `User.Password`).
- Include all the **sub collections** of the entity where every item in the collection is a simple **relation DTO**. - Include all the **sub collections** of the entity where every item in the collection is a simple **relation DTO**.
- Inherit from one of the **extensible entity DTO** classes for aggregate roots (and entities implement the `IHasExtraProperties`).
Example: Example:
```c# ```c#
[Serializable] [Serializable]
public class IssueDto : FullAuditedEntityDto<Guid> public class IssueDto : ExtensibleFullAuditedEntityDto<Guid>
{ {
public string Title { get; set; } public string Title { get; set; }
public string Text { get; set; } public string Text { get; set; }
@ -57,7 +58,7 @@ Example:
````C# ````C#
[Serializable] [Serializable]
public class IssueWithDetailsDto : FullAuditedEntityDto<Guid> public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto<Guid>
{ {
public string Title { get; set; } public string Title { get; set; }
public string Text { get; set; } public string Text { get; set; }
@ -66,14 +67,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto<Guid>
} }
[Serializable] [Serializable]
public class MilestoneDto : EntityDto<Guid> public class MilestoneDto : ExtensibleEntityDto<Guid>
{ {
public string Name { get; set; } public string Name { get; set; }
public bool IsClosed { get; set; } public bool IsClosed { get; set; }
} }
[Serializable] [Serializable]
public class LabelDto : EntityDto<Guid> public class LabelDto : ExtensibleEntityDto<Guid>
{ {
public string Name { get; set; } public string Name { get; set; }
public string Color { get; set; } public string Color { get; set; }
@ -120,6 +121,7 @@ Task<List<QuestionWithDetailsDto>> GetListAsync(QuestionListQueryDto queryDto);
* **Do** use the `CreateAsync` **method name**. * **Do** use the `CreateAsync` **method name**.
* **Do** get a **specialized input** DTO to create the entity. * **Do** get a **specialized input** DTO to create the entity.
* **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed.
* **Do** use **data annotations** for input validation. * **Do** use **data annotations** for input validation.
* Share constants between domain wherever possible (via constants defined in the **domain shared** package). * Share constants between domain wherever possible (via constants defined in the **domain shared** package).
* **Do** return **the detailed** DTO for new created entity. * **Do** return **the detailed** DTO for new created entity.
@ -135,10 +137,11 @@ The related **DTO**:
````C# ````C#
[Serializable] [Serializable]
public class CreateQuestionDto public class CreateQuestionDto : ExtensibleObject
{ {
[Required] [Required]
[StringLength(QuestionConsts.MaxTitleLength, MinimumLength = QuestionConsts.MinTitleLength)] [StringLength(QuestionConsts.MaxTitleLength,
MinimumLength = QuestionConsts.MinTitleLength)]
public string Title { get; set; } public string Title { get; set; }
[StringLength(QuestionConsts.MaxTextLength)] [StringLength(QuestionConsts.MaxTextLength)]
@ -152,6 +155,7 @@ public class CreateQuestionDto
- **Do** use the `UpdateAsync` **method name**. - **Do** use the `UpdateAsync` **method name**.
- **Do** get a **specialized input** DTO to update the entity. - **Do** get a **specialized input** DTO to update the entity.
- **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed.
- **Do** get the Id of the entity as a separated primitive parameter. Do not include to the update DTO. - **Do** get the Id of the entity as a separated primitive parameter. Do not include to the update DTO.
- **Do** use **data annotations** for input validation. - **Do** use **data annotations** for input validation.
- Share constants between domain wherever possible (via constants defined in the **domain shared** package). - Share constants between domain wherever possible (via constants defined in the **domain shared** package).
@ -200,6 +204,10 @@ This method votes a question and returns the current score of the question.
* **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source. * **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source.
#### Extra Properties
* **Do** use either `MapExtraPropertiesTo` extension method ([see](Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services.
#### Manipulating / Deleting Entities #### Manipulating / Deleting Entities
* **Do** always get all the related entities from repositories to perform the operations on them. * **Do** always get all the related entities from repositories to perform the operations on them.

1
docs/en/Best-Practices/Data-Transfer-Objects.md

@ -2,6 +2,7 @@
* **Do** define DTOs in the **application contracts** package. * **Do** define DTOs in the **application contracts** package.
* **Do** inherit from the pre-built **base DTO classes** where possible and necessary (like `EntityDto<TKey>`, `CreationAuditedEntityDto<TKey>`, `AuditedEntityDto<TKey>`, `FullAuditedEntityDto<TKey>` and so on). * **Do** inherit from the pre-built **base DTO classes** where possible and necessary (like `EntityDto<TKey>`, `CreationAuditedEntityDto<TKey>`, `AuditedEntityDto<TKey>`, `FullAuditedEntityDto<TKey>` and so on).
* **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto<TKey>`), because aggregate roots are extensible objects and extra properties are mapped to DTOs in this way.
* **Do** define DTO members with **public getter and setter**. * **Do** define DTO members with **public getter and setter**.
* **Do** use **data annotations** for **validation** on the properties of DTOs those are inputs of the service. * **Do** use **data annotations** for **validation** on the properties of DTOs those are inputs of the service.
* **Do** not add any **logic** into DTOs except implementing `IValidatableObject` when necessary. * **Do** not add any **logic** into DTOs except implementing `IValidatableObject` when necessary.

9
docs/en/Best-Practices/Entity-Framework-Core-Integration.md

@ -89,13 +89,15 @@ public static class IdentityDbContextModelBuilderExtensions
builder.Entity<IdentityUser>(b => builder.Entity<IdentityUser>(b =>
{ {
b.ToTable(options.TablePrefix + "Users", options.Schema); b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity //code omitted for brevity
}); });
builder.Entity<IdentityUserClaim>(b => builder.Entity<IdentityUserClaim>(b =>
{ {
b.ToTable(options.TablePrefix + "UserClaims", options.Schema); b.ToTable(options.TablePrefix + "UserClaims", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity //code omitted for brevity
}); });
@ -104,10 +106,11 @@ public static class IdentityDbContextModelBuilderExtensions
} }
```` ````
* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example: * **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above).
* **Do** create a **configuration options** class by inheriting from the `AbpModelBuilderConfigurationOptions`. Example:
````C# ````C#
public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions public class IdentityModelBuilderConfigurationOptions : AbpModelBuilderConfigurationOptions
{ {
public IdentityModelBuilderConfigurationOptions() public IdentityModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema)

4
docs/en/Best-Practices/MongoDB-Integration.md

@ -90,11 +90,11 @@ public static class AbpIdentityMongoDbContextExtensions
} }
``` ```
- **Do** create a **configuration options** class by inheriting from the `MongoModelBuilderConfigurationOptions`. Example: - **Do** create a **configuration options** class by inheriting from the `AbpMongoModelBuilderConfigurationOptions`. Example:
```c# ```c#
public class IdentityMongoModelBuilderConfigurationOptions public class IdentityMongoModelBuilderConfigurationOptions
: MongoModelBuilderConfigurationOptions : AbpMongoModelBuilderConfigurationOptions
{ {
public IdentityMongoModelBuilderConfigurationOptions() public IdentityMongoModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix) : base(AbpIdentityConsts.DefaultDbTablePrefix)

10
docs/en/CLI.md

@ -60,6 +60,8 @@ abp new Acme.BookStore
* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version. * `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version.
* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D\localTemplate` or `https://<your url>.zip`). * `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D\localTemplate` or `https://<your url>.zip`).
* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. * `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder.
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. You can set your own connection string if you don't want to use the default. Be aware that the default database provider is `SQL Server`, therefore you can only enter connection string for SQL Server!
* `--local-framework-ref --abp-path`: keeps local references to projects instead of replacing with NuGet package references.
### add-package ### add-package
@ -133,6 +135,8 @@ abp update [options]
* `--include-previews` or `-p`: Includes preview, beta and rc packages while checking the latest versions. * `--include-previews` or `-p`: Includes preview, beta and rc packages while checking the latest versions.
* `--npm`: Only updates NPM packages. * `--npm`: Only updates NPM packages.
* `--nuget`: Only updates NuGet packages. * `--nuget`: Only updates NuGet packages.
* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default
* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default.
### switch-to-preview ### switch-to-preview
@ -168,7 +172,11 @@ Some features of the CLI requires to be logged in to abp.io platform. To login w
abp login <username> abp login <username>
``` ```
Notice that, a new login with an already active session, will kill the previous session and creates a new one. ```bash
abp login <username> -p <password>
```
Notice that, a new login with an already active session, overwrites the previous session.
### logout ### logout

4
docs/en/Contribution/Localization-Text-Files.md

@ -23,8 +23,8 @@ Here, a list of localization text files for anyone wants to contribute to locali
* https://github.com/abpframework/abp/tree/master/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json * https://github.com/abpframework/abp/tree/master/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json
* https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json * https://github.com/abpframework/abp/tree/master/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json
* https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json * https://github.com/abpframework/abp/tree/master/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json
* https://github.com/abpframework/abp/tree/master/samples/BookStore/src/Acme.BookStore.Domain.Shared/Localization/BookStore/en.json * https://github.com/abpframework/abp-samples/tree/master/BookStore/src/Acme.BookStore.Domain.Shared/Localization/BookStore/en.json
* https://github.com/abpframework/abp/tree/master/samples/DashboardDemo/src/DashboardDemo.Domain.Shared/Localization/DashboardDemo/en.json * https://github.com/abpframework/abp-samples/tree/master/DashboardDemo/src/DashboardDemo.Domain.Shared/Localization/DashboardDemo/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application.Contracts/ProductManagement/Localization/ApplicationContracts/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Application.Contracts/ProductManagement/Localization/ApplicationContracts/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Localization/Domain/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Domain/ProductManagement/Localization/Domain/en.json
* https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Web/Localization/Resources/ProductManagement/en.json * https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo/modules/product/src/ProductManagement.Web/Localization/Resources/ProductManagement/en.json

38
docs/en/Customizing-Application-Modules-Extending-Entities.md

@ -4,7 +4,7 @@ In some cases, you may want to add some additional properties (and database fiel
## Extra Properties ## Extra Properties
[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities. [Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects.
Example: Example:
@ -25,11 +25,38 @@ Extra properties are stored as a single `JSON` formatted string value in the dat
See the [entities document](Entities.md) for more about the extra properties system. See the [entities document](Entities.md) for more about the extra properties system.
> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below. > It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above.
## Creating a New Entity Maps to the Same Database Table/Collection ## Entity Extensions (EF Core)
As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to;
* Create **indexes** and **foreign keys** for an extra property.
* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example).
* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more).
To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table.
While using the extra properties approach is **easy to use** and suitable for some scenarios, it has some drawbacks described in the [entities document](Entities.md). Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `ObjectExtensionManager`:
````csharp
ObjectExtensionManager.Instance
.MapEfCoreProperty<IdentityUser, string>(
"SocialSecurityNumber",
b => { b.HasMaxLength(32); }
);
````
* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table).
* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties).
> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEfCoreEntityExtensionMappings`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself.
Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database.
You can then use the same extra properties system defined in the previous section to manipulate the property over the entity.
## Creating a New Entity Maps to the Same Database Table/Collection
Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database).
@ -146,4 +173,5 @@ public class MyDistributedIdentityUserCreatedEventHandler :
## See Also ## See Also
* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) * [Migration System for the EF Core](Entity-Framework-Core-Migrations.md)
* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md)

102
docs/en/Customizing-Application-Modules-Overriding-Services.md

@ -60,6 +60,7 @@ In most cases, you will want to change one or a few methods of the current imple
````csharp ````csharp
[Dependency(ReplaceServices = true)] [Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService public class MyIdentityUserAppService : IdentityUserAppService
{ {
//... //...
@ -161,6 +162,105 @@ Check the [localization system](Localization.md) to learn how to localize the er
Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above.
## Extending Data Transfer Objects
**Extending [entities](Entities.md)** is possible as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md). In this way, you can add **custom properties** to entities and perform **additional business logic** by overriding the related services as described above.
It is also possible to extend Data Transfer Objects (**DTOs**) used by the application services. In this way, you can get extra properties from the UI (or client) and return extra properties from the service.
### Example
Assuming that you've already added a `SocialSecurityNumber` as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md) and want to include this information while getting the list of users from the `GetListAsync` method of the `IdentityUserAppService`.
You can use the [object extension system](Object-Extensions.md) to add the property to the `IdentityUserDto`. Write this code inside the `YourProjectNameDtoExtensions` class comes with the application startup template:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserDto, string>(
"SocialSecurityNumber"
);
````
This code defines a `SocialSecurityNumber` to the `IdentityUserDto` class as a `string` type. That's all. Now, if you call the `/api/identity/users` HTTP API (which uses the `IdentityUserAppService` internally) from a REST API client, you will see the `SocialSecurityNumber` value in the `extraProperties` section.
````json
{
"totalCount": 1,
"items": [{
"tenantId": null,
"userName": "admin",
"name": "admin",
"surname": null,
"email": "admin@abp.io",
"emailConfirmed": false,
"phoneNumber": null,
"phoneNumberConfirmed": false,
"twoFactorEnabled": false,
"lockoutEnabled": true,
"lockoutEnd": null,
"concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c",
"isDeleted": false,
"deleterId": null,
"deletionTime": null,
"lastModificationTime": "2020-04-09T21:25:47.0740706",
"lastModifierId": null,
"creationTime": "2020-04-09T21:25:46.8308744",
"creatorId": null,
"id": "8edecb8f-1894-a9b1-833b-39f4725db2a3",
"extraProperties": {
"SocialSecurityNumber": "123456789"
}
}]
}
````
Manually added the `123456789` value to the database for now.
All pre-built modules support extra properties in their DTOs, so you can configure easily.
### Definition Check
When you [define](Customizing-Application-Modules-Extending-Entities.md) an extra property for an entity, it doesn't automatically appear in all the related DTOs, because of the security. The extra property may contain a sensitive data and you may not want to expose it to the clients by default.
So, you need to explicitly define the same property for the corresponding DTO if you want to make it available for the DTO (as just done above). If you want to allow to set it on user creation, you also need to define it for the `IdentityUserCreateDto`.
If the property is not so secure, this can be tedious. Object extension system allows you to ignore this definition check for a desired property. See the example below:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
options.CheckPairDefinitionOnMapping = false;
}
);
````
This is another approach to define a property for an entity (`ObjectExtensionManager` has more, see [its document](Object-Extensions.md)). This time, we set `CheckPairDefinitionOnMapping` to false to skip definition check while mapping entities to DTOs and vice verse.
If you don't like this approach but want to add a single property to multiple objects (DTOs) easier, `AddOrUpdateProperty` can get an array of types to add the extra property:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<string>(
new[]
{
typeof(IdentityUserDto),
typeof(IdentityUserCreateDto),
typeof(IdentityUserUpdateDto)
},
"SocialSecurityNumber"
);
````
### About the User Interface
This system allows you to add extra properties to entities and DTOs and execute custom business code, however it does nothing related to the User Interface.
See [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) guide for the UI part.
## How to Find the Services? ## How to Find the Services?
[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. [Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services.

11
docs/en/Entities.md

@ -373,16 +373,19 @@ So, you can directly use the `ExtraProperties` property to use the dictionary A
The way to store this dictionary in the database depends on the database provider you're using. The way to store this dictionary in the database depends on the database provider you're using.
* For [Entity Framework Core](Entity-Framework-Core.md), it is stored in a single `ExtraProperties` field as a `JSON` string. Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core. * For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations;
* By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core.
* If you want, you can use the `ObjectExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `ObjectExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `ObjectExtensionManager`.
* For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system. * For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system.
### Discussion for the Extra Properties ### Discussion for the Extra Properties
Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. You normally **don't need** to this system for your own entities, because it has the following drawbacks: Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way.
* It is **not fully type safe**. You normally **don't need** to this system for your own entities, because it has the following drawbacks:
* It is **not fully type safe** since it works with strings as property names.
* It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects. * It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects.
* It **doesn't create fields** in the database table for EF Core, so it will not be easy to create indexes or search/order by this field in the database side.
### Extra Properties Behind Entities ### Extra Properties Behind Entities

109
docs/en/Entity-Framework-Core-Migrations.md

@ -6,7 +6,7 @@ This document begins by **introducing the default structure** provided by [the a
### Source Code ### Source Code
You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code.
## About the EF Core Code First Migrations ## About the EF Core Code First Migrations
@ -93,7 +93,7 @@ From the database point of view, there are three important projects those will b
This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application. This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application.
**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your custom [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity: **Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity:
````csharp ````csharp
[ConnectionStringName("Default")] [ConnectionStringName("Default")]
@ -117,15 +117,15 @@ public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
builder.Entity<AppUser>(b => builder.Entity<AppUser>(b =>
{ {
//Sharing the same table "AbpUsers" with the IdentityUser //Sharing the same Users table with the IdentityUser
b.ToTable("AbpUsers"); b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users");
//Configure base properties
b.ConfigureByConvention(); b.ConfigureByConvention();
b.ConfigureAbpUser(); b.ConfigureAbpUser();
//Moved customization of the "AbpUsers" table to an extension method /* Configure mappings for your additional properties
b.ConfigureCustomUserProperties(); * Also see the MyProjectNameEntityExtensions class
*/
}); });
/* Configure your own tables/entities inside the ConfigureBookStore method */ /* Configure your own tables/entities inside the ConfigureBookStore method */
@ -188,12 +188,6 @@ public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbCo
builder.ConfigureFeatureManagement(); builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement(); builder.ConfigureTenantManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureBookStore method */ /* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore(); builder.ConfigureBookStore();
} }
@ -274,7 +268,7 @@ In this way, the mapping configuration of a module can be shared between `DbCont
You may want to **reuse a table** of a depended module in your application. In this case, you have two options: You may want to **reuse a table** of a depended module in your application. In this case, you have two options:
1. You can **directly use the entity** defined by the module. 1. You can **directly use the entity** defined by the module (you can still [extend the entity](Customizing-Application-Modules-Extending-Entities.md) in some level).
2. You can **create a new entity** mapping to the same database table. 2. You can **create a new entity** mapping to the same database table.
###### Use the Entity Defined by a Module ###### Use the Entity Defined by a Module
@ -376,10 +370,8 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<AppRole>(b => builder.Entity<AppRole>(b =>
{ {
b.ToTable("AbpRoles"); b.ToTable("AbpRoles");
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Title).HasMaxLength(128);
b.ConfigureCustomRoleProperties();
}); });
... ...
@ -395,69 +387,45 @@ We added the following lines:
````csharp ````csharp
builder.Entity<AppRole>(b => builder.Entity<AppRole>(b =>
{ {
b.ToTable("AbpRoles"); b.ToTable("AbpRoles");
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Title).HasMaxLength(128);
b.ConfigureCustomRoleProperties();
}); });
```` ````
* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. * It maps to the same `AbpRoles` table shared with the `IdentityRole` entity.
* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it. * `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it.
`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project): You've configured the custom property for your `DbContext` that is used by your application on the runtime. We also need to configure the `MigrationsDbContext`.
````csharp
public static void ConfigureCustomRoleProperties<TRole>(this EntityTypeBuilder<TRole> b)
where TRole : class, IEntity<Guid>
{
b.Property<string>(nameof(AppRole.Title)).HasMaxLength(128);
}
````
* This method only defines the **custom properties** of your entity.
* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table.
You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`. Instead of directly changing the `MigrationsDbContext`, we should use the entity extension system of the ABP Framework. Find the `YourProjectNameEntityExtensions` class in the `.EntityFrameworkCore` project of your solution (`BookStoreEntityExtensions` for this example) and change it as shown below:
Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below:
````csharp ````csharp
protected override void OnModelCreating(ModelBuilder builder) public static class MyProjectNameEntityExtensions
{ {
base.OnModelCreating(builder); private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
/* Include modules to your migration db context */ public static void Configure()
...
/* Configure customizations for entities from the modules included */
//CONFIGURE THE CUSTOM ROLE PROPERTIES
builder.Entity<IdentityRole>(b =>
{ {
b.ConfigureCustomRoleProperties(); OneTimeRunner.Run(() =>
}); {
ObjectExtensionManager.Instance
... .MapEfCoreProperty<IdentityRole, string>(
"Title",
/* Configure your own tables/entities inside the ConfigureBookStore method */ builder => { builder.HasMaxLength(64); }
);
builder.ConfigureBookStore(); });
}
} }
```` ````
Only added the following lines: > Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)` or use a constant string.
````csharp `ObjectExtensionManager` is used to add properties to existing entities. Since `ObjectExtensionManager.Instance` is a static instance (singleton), we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework.
builder.Entity<IdentityRole>(b =>
{ See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system.
b.ConfigureCustomRoleProperties();
});
````
In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity. > We've repeated a similar database mapping code, like `HasMaxLength(128)`, in both classes.
Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project): Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project):
@ -536,7 +504,7 @@ Instead of creating a new entity class to add a custom property, you can use the
###### Using the ExtraProperties ###### Using the ExtraProperties
All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property, which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity. All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property (because they implement the `IHasExtraProperties` interface), which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity.
For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example: For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example:
@ -553,16 +521,13 @@ public class IdentityRoleExtendingService : ITransientDependency
public async Task<string> GetTitleAsync(Guid id) public async Task<string> GetTitleAsync(Guid id)
{ {
var role = await _identityRoleRepository.GetAsync(id); var role = await _identityRoleRepository.GetAsync(id);
return role.GetProperty<string>("Title"); return role.GetProperty<string>("Title");
} }
public async Task SetTitleAsync(Guid id, string newTitle) public async Task SetTitleAsync(Guid id, string newTitle)
{ {
var role = await _identityRoleRepository.GetAsync(id); var role = await _identityRoleRepository.GetAsync(id);
role.SetProperty("Title", newTitle); role.SetProperty("Title", newTitle);
await _identityRoleRepository.UpdateAsync(role); await _identityRoleRepository.UpdateAsync(role);
} }
} }
@ -575,12 +540,20 @@ In this way, you can easily attach any type of value to an entity of a depended
* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields. * All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields.
* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors. * Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors.
###### Using the Entity Extensions System
Entity extension system solves the main problem of the extra properties: It can store an extra property in a **standard table field** in the database.
All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
###### Creating a New Table ###### Creating a New Table
Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table. Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table.
In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here). In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here).
> See the "[extending entities](Customizing-Application-Modules-Extending-Entities.md)" guide for more details on extending entities, including data duplication and synchronization tips.
#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path #### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path
As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path.
@ -909,4 +882,4 @@ This document explains how to split your databases and manage your database migr
## Source Code ## Source Code
You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code.

112
docs/en/Entity-Framework-Core.md

@ -58,6 +58,53 @@ namespace MyCompany.MyProject
} }
```` ````
### About the EF Core Fluent Mapping
The [application startup template](Startup-Templates/Application.md) has been configured to use the [EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/) to map your entities to your database tables.
You can still use the **data annotation attributes** (like `[Required]`) on the properties of your entity while the ABP documentation generally follows the **fluent mapping API** approach. It is up to you.
ABP Framework has some **base entity classes** and **conventions** (see the [entities document](Entities.md)) and it provides some useful **extension methods** to configure the properties inherited from the base entity classes.
#### ConfigureByConvention Method
`ConfigureByConvention()` is the main extension method that **configures all the base properties** and conventions for your entities. So, it is a **best practice** to call this method for all your entities, in your fluent mapping code.
**Example**: Assume that you've a `Book` entity derived from `AggregateRoot<Guid>` base class:
````csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
}
````
You can override the `OnModelCreating` method in your `DbContext` and configure the mapping as shown below:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
//Always call the base method
base.OnModelCreating(builder);
builder.Entity<Book>(b =>
{
b.ToTable("Books");
//Configure the base properties
b.ConfigureByConvention();
//Configure other properties (if you are using the fluent API)
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
````
* Calling `b.ConfigureByConvention()` is important here to properly **configure the base properties**.
* You can configure the `Name` property here or you can use the **data annotation attributes** (see the [EF Core document](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)).
> While there are many extension methods to configure your base properties, `ConfigureByConvention()` internally calls them if necessary. So, it is enough to call it.
### Configure the Connection String Selection ### Configure the Connection String Selection
If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example: If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example:
@ -225,7 +272,7 @@ public override async Task DeleteAsync(
} }
```` ````
### Access to the EF Core API ## Access to the EF Core API
In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example:
@ -251,9 +298,60 @@ public class BookService
> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case. > Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case.
### Advanced Topics ## Extra Properties & Object Extension Manager
Extra Properties system allows you to set/get dynamic properties to entities those implement the `IHasExtraProperties` interface. It is especially useful when you want to add custom properties to the entities defined in an [application module](Modules/Index.md), when you use the module as package reference.
By default, all the extra properties of an entity are stored as a single `JSON` object in the database.
#### Set Default Repository Classes Entity extension system allows you to to store desired extra properties in separate fields in the related database table. For more information about the extra properties & the entity extension system, see the following documents:
* [Customizing the Application Modules: Extending Entities](Customizing-Application-Modules-Extending-Entities.md)
* [Entities](Entities.md)
This section only explains the EF Core related usage of the `ObjectExtensionManager`.
### ObjectExtensionManager.Instance
`ObjectExtensionManager` implements the singleton pattern, so you need to use the static `ObjectExtensionManager.Instance` to perform all the operations.
### MapEfCoreProperty
`MapEfCoreProperty` is a shortcut extension method to define an extension property for an entity and map to the database.
**Example**: Add `Title` property (database field) to the `IdentityRole` entity:
````csharp
ObjectExtensionManager.Instance
.MapEfCoreProperty<IdentityRole, string>(
"Title",
builder => { builder.HasMaxLength(64); }
);
````
If the related module has implemented this feature (by using the `ConfigureEfCoreEntity` explained below), then the new property is added to the model. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field.
>`MapEfCoreProperty` method must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEntityExtensions` class that is safe to use this method inside.
### ConfigureEfCoreEntity
If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureEfCoreEntity` extension method in your entity mapping. However, there is a shortcut extension method `ConfigureObjectExtensions` that can be used while configuring the entity mapping:
````csharp
builder.Entity<YourEntity>(b =>
{
b.ConfigureObjectExtensions();
//...
});
````
> If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` for this example), ABP Framework internally calls the `ConfigureObjectExtensions` method. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention.
See the "*ConfigureByConvention Method*" section above for more information.
## Advanced Topics
### Set Default Repository Classes
Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations. Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations.
@ -299,7 +397,7 @@ context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
}); });
``` ```
#### Set Base DbContext Class or Interface for Default Repositories ### Set Base DbContext Class or Interface for Default Repositories
If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example: If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example:
@ -331,7 +429,7 @@ public class BookRepository : EfCoreRepository<IBookStoreDbContext, Book, Guid>,
One advantage of using an interface for a DbContext is then it will be replaceable by another implementation. One advantage of using an interface for a DbContext is then it will be replaceable by another implementation.
#### Replace Other DbContextes ### Replace Other DbContextes
Once you properly define and use an interface for DbContext, then any other implementation can replace it using the `ReplaceDbContext` option: Once you properly define and use an interface for DbContext, then any other implementation can replace it using the `ReplaceDbContext` option:
@ -344,3 +442,7 @@ context.Services.AddAbpDbContext<OtherDbContext>(options =>
```` ````
In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime.
## See Also
* [Entities](Entities.md)

52
docs/en/Exception-Handling.md

@ -1,4 +1,4 @@
## Exception Handling # Exception Handling
ABP provides a built-in infrastructure and offers a standard model for handling exceptions in a web application. ABP provides a built-in infrastructure and offers a standard model for handling exceptions in a web application.
@ -7,7 +7,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling
* Provides a configurable way to **localize** exception messages. * Provides a configurable way to **localize** exception messages.
* Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map these to custom exceptions. * Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map these to custom exceptions.
### Automatic Exception Handling ## Automatic Exception Handling
`AbpExceptionFilter` handles an exception if **any of the following conditions** are met: `AbpExceptionFilter` handles an exception if **any of the following conditions** are met:
@ -17,7 +17,7 @@ ABP provides a built-in infrastructure and offers a standard model for handling
If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client. If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client.
#### Error Message Format ### Error Message Format
Error Message is an instance of the `RemoteServiceErrorResponse` class. The simplest error JSON has a **message** property as shown below: Error Message is an instance of the `RemoteServiceErrorResponse` class. The simplest error JSON has a **message** property as shown below:
@ -83,11 +83,11 @@ Error **details** in an optional field of the JSON error message. Thrown `Except
`AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic. `AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic.
#### Logging ### Logging
Caught exceptions are automatically logged. Caught exceptions are automatically logged.
##### Log Level #### Log Level
Exceptions are logged with the `Error` level by default. The Log level can be determined by the exception if it implements the `IHasLogLevel` interface. Example: Exceptions are logged with the `Error` level by default. The Log level can be determined by the exception if it implements the `IHasLogLevel` interface. Example:
@ -100,7 +100,7 @@ public class MyException : Exception, IHasLogLevel
} }
```` ````
##### Self Logging Exceptions #### Self Logging Exceptions
Some exception types may need to write additional logs. They can implement the `IExceptionWithSelfLogging` if needed. Example: Some exception types may need to write additional logs. They can implement the `IExceptionWithSelfLogging` if needed. Example:
@ -116,7 +116,7 @@ public class MyException : Exception, IExceptionWithSelfLogging
> `ILogger.LogException` extension methods is used to write exception logs. You can use the same extension method when needed. > `ILogger.LogException` extension methods is used to write exception logs. You can use the same extension method when needed.
### Business Exceptions ## Business Exceptions
Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception. Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception.
@ -145,11 +145,11 @@ Volo.Qa:010002
* You can **directly throw** a `BusinessException` or **derive** your own exception types from it when needed. * You can **directly throw** a `BusinessException` or **derive** your own exception types from it when needed.
* All properties are optional for the `BusinessException` class. But you generally set either `ErrorCode` or `Message` property. * All properties are optional for the `BusinessException` class. But you generally set either `ErrorCode` or `Message` property.
### Exception Localization ## Exception Localization
One problem with throwing exceptions is how to localize error messages while sending it to the client. ABP offers two models and their variants. One problem with throwing exceptions is how to localize error messages while sending it to the client. ABP offers two models and their variants.
#### User Friendly Exception ### User Friendly Exception
If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client. If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client.
@ -192,7 +192,7 @@ Then the localization text can be:
* The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class. * The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class.
#### Using Error Codes ### Using Error Codes
`UserFriendlyException` is fine, but it has a few problems in advanced usages: `UserFriendlyException` is fine, but it has a few problems in advanced usages:
@ -230,7 +230,7 @@ throw new BusinessException(QaDomainErrorCodes.CanNotVoteYourOwnAnswer);
* Throwing any exception implementing the `IHasErrorCode` interface behaves the same. So, the error code localization approach is not unique to the `BusinessException` class. * Throwing any exception implementing the `IHasErrorCode` interface behaves the same. So, the error code localization approach is not unique to the `BusinessException` class.
* Defining localized string is not required for an error message. If it's not defined, ABP sends the default error message to the client. It does not use the `Message` property of the exception! if you want that, use the `UserFriendlyException` (or use an exception type that implements the `IUserFriendlyException` interface). * Defining localized string is not required for an error message. If it's not defined, ABP sends the default error message to the client. It does not use the `Message` property of the exception! if you want that, use the `UserFriendlyException` (or use an exception type that implements the `IUserFriendlyException` interface).
##### Using Message Parameters #### Using Message Parameters
If you have a parameterized error message, then you can set it with the exception's `Data` property. For example: If you have a parameterized error message, then you can set it with the exception's `Data` property. For example:
@ -265,7 +265,7 @@ Then the localized text can contain the `UserName` parameter:
* `WithData` can be chained with more than one parameter (like `.WithData(...).WithData(...)`). * `WithData` can be chained with more than one parameter (like `.WithData(...).WithData(...)`).
### HTTP Status Code Mapping ## HTTP Status Code Mapping
ABP tries to automatically determine the most suitable HTTP status code for common exception types by following these rules: ABP tries to automatically determine the most suitable HTTP status code for common exception types by following these rules:
@ -280,7 +280,7 @@ ABP tries to automatically determine the most suitable HTTP status code for comm
The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP status code. The default implementation is the `DefaultHttpExceptionStatusCodeFinder` class. It can be replaced or extended as needed. The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP status code. The default implementation is the `DefaultHttpExceptionStatusCodeFinder` class. It can be replaced or extended as needed.
#### Custom Mappings ### Custom Mappings
Automatic HTTP status code determination can be overrided by custom mappings. For example: Automatic HTTP status code determination can be overrided by custom mappings. For example:
@ -291,12 +291,32 @@ services.Configure<AbpExceptionHttpStatusCodeOptions>(options =>
}); });
```` ````
### Built-In Exceptions ## Subscribing to the Exceptions
It is possible to be informed when the ABP Framework **handles an exception**. It automatically **logs** all the exceptions to the standard [logger](Logging.md), but you may want to do more.
In this case, create a class derived from the `ExceptionSubscriber` class in your application:
````csharp
public class MyExceptionSubscriber : ExceptionSubscriber
{
public override async Task HandleAsync(ExceptionNotificationContext context)
{
//TODO...
}
}
````
The `context` object contains necessary information about the exception occurred.
> You can have multiple subscribers, each gets a copy of the exception. Exceptions thrown by your subscriber is ignored (but still logged).
## Built-In Exceptions
Some exception types are automatically thrown by the framework: Some exception types are automatically thrown by the framework:
- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See authorization document (TODO: link) for more. - `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](Authorization.md) for more.
- `AbpValidationException` is thrown if the input of the current request is not valid. See validation document (TODO: link) for more. - `AbpValidationException` is thrown if the input of the current request is not valid. See [validation](Validation.md) for more.
- `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](Repositories.md). - `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](Repositories.md).
You can also throw these type of exceptions in your code (although it's rarely needed). You can also throw these type of exceptions in your code (although it's rarely needed).

128
docs/en/Getting-Started-Angular-Template.md

@ -1,126 +1,8 @@
## Getting Started With the Angular Application Template # Getting Started with the Startup Templates
This tutorial explains how to create a new Angular application using the startup template, configure and run it. See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates:
### Creating a New Project * [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No)
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No)
This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. <!-- TODO: this document has been moved, it should be deleted in the future. -->
Install the ABP CLI using a command line window, if you've not installed before:
````bash
dotnet tool install -g Volo.Abp.Cli
````
Use `abp new` command in an empty folder to create your project:
````bash
abp new Acme.BookStore -u angular
````
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.
`-u angular` option specifies the UI framework to be Angular. Default database provider is EF Core. See the [CLI documentation](CLI.md) for all available options.
#### Pre Requirements
The created solution requires;
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/)
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
### The Solution Structure
Open the solution in **Visual Studio**:
![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-for-spa.png)
The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database.
> See the [Application Template Document](Startup-Templates/Application.md) to understand the solution structure in details.
### Database Connection String
Check the **connection string** in the `appsettings.json` file under the `.HttpApi.Host` project:
````json
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
}
````
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need.
### Create Database & Apply Database Migrations
You have two options to create the database.
#### Using the DbMigrator Application
The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment.
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one.
Right click to the `.DbMigrator` project and select **Set as StartUp Project**:
![set-as-startup-project](images/set-as-startup-project.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
![set-as-startup-project](images/db-migrator-app.png)
#### Using EF Core Update-Database Command
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**:
![set-as-startup-project](images/set-as-startup-project.png)
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command:
![pcm-update-database](images/pcm-update-database-v2.png)
This will create a new database based on the configured connection string.
> Using the `.DbMigrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application.
### Running the Application
#### Run the API Host (Server Side)
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI:
![bookstore-homepage](images/bookstore-swagger-ui-host.png)
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI.
##### Authorization for the Swagger UI
Most of the application APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too.
#### Run the Angular Application (Client Side)
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest the [yarn](https://yarnpkg.com) package manager while `npm install` will also work in most cases)
````bash
yarn
````
Once all node modules are loaded, execute `yarn start` or `npm start` command:
````bash
yarn start
````
Open your favorite browser and go to `localhost:4200` URL. Initial username is `admin` and password is `1q2w3E*`.
The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**.
> We recommend [Visual Studio Code](https://code.visualstudio.com/) as the editor for the Angular project, but you are free to use your favorite editor.
### What's Next?
* [Application development tutorial](Tutorials/Part-1)

2
docs/en/Getting-Started-AspNetCore-Application.md

@ -153,5 +153,5 @@ namespace BasicAspNetCoreApplication
## Source Code ## Source Code
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp/tree/master/samples/BasicAspNetCoreApplication). Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication).

106
docs/en/Getting-Started-AspNetCore-MVC-Template.md

@ -1,104 +1,8 @@
## Getting Started With the ASP.NET Core MVC Template # Getting Started with the Startup Templates
This tutorial explains how to create a new ASP.NET Core MVC web application using the startup template, configure and run it. See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates:
### Creating a New Project * [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No)
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No)
This tutorial uses **ABP CLI** to create a new project. See the [Get Started](https://abp.io/get-started) page for other options. <!-- TODO: this document has been moved, it should be deleted in the future. -->
Install the ABP CLI using a command line window, if you've not installed before:
````bash
dotnet tool install -g Volo.Abp.Cli
````
Use `abp new` command in an empty folder to create your project:
````bash
abp new Acme.BookStore
````
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.
`new` command creates a **layered MVC application** with **Entity Framework Core** as the database provider. However, it has additional options. See the [CLI documentation](CLI.md) for all available options.
#### Pre Requirements
The created solution requires;
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/)
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
### The Solution Structure
Open the solution in **Visual Studio**:
![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png)
The solution has a layered structure (based on [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects properly configured to work with **EF Core** & **SQLite in-memory** database.
> See [Application template document](Startup-Templates/Application.md) to understand the solution structure in details.
### Database Connection String
Check the **connection string** in the `appsettings.json` file under the `.Web` project:
````json
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
}
````
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use another DBMS if you want. Change the connection string if you need.
### Create Database & Apply Database Migrations
You have two options to create the database.
#### Using the DbMigrator Application
The solution contains a console application (named `Acme.BookStore.DbMigrator` in this sample) that can create database, apply migrations and seed initial data. It is useful on development as well as on production environment.
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one.
Right click to the `.DbMigrator` project and select **Set as StartUp Project**:
![set-as-startup-project](images/set-as-startup-project.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
![set-as-startup-project](images/db-migrator-app.png)
#### Using EF Core Update-Database Command
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. Right click to the `.Web` project and select **Set as StartUp Project**:
![set-as-startup-project](images/set-as-startup-project.png)
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command:
![pcm-update-database](images/pcm-update-database-v2.png)
This will create a new database based on the configured connection string.
> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application.
### Running the Application
Ensure that the `.Web` project is the startup project. Run the application which will open the **home** page in your browser:
![bookstore-homepage](images/bookstore-homepage.png)
Click the **Login** button, enter `admin` as the username and `1q2w3E*` as the password to login to the application.
The startup template includes the **identity management** and **tenant management** modules. Once you login, the Administration menu will be available where you can manage **tenants**, **roles**, **users** and their **permissions**. User management page is shown below:
![bookstore-user-management](images/bookstore-user-management-v2.png)
### What's Next?
* [Application development tutorial](Tutorials/Part-1.md)

2
docs/en/Getting-Started-Console-Application.md

@ -178,4 +178,4 @@ Just called `options.UseAutofac()` method in the `AbpApplicationFactory.Create`
## Source Code ## Source Code
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp/tree/master/samples/BasicConsoleApplication). Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication).

8
docs/en/Getting-Started-With-Startup-Templates.md

@ -0,0 +1,8 @@
# Getting Started with the Startup Templates
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates:
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started?UI=MVC&DB=EF&Tiered=No)
* [Getting Started with the Angular UI](Getting-Started?UI=NG&DB=EF&Tiered=No)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

407
docs/en/Getting-Started.md

@ -0,0 +1,407 @@
# Getting started
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"]
}
````
This tutorial explains how to create a new {{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}} application using the startup template, configure and run it.
## Setup your development environment
First things first! Let's setup your development environment before creating the first project.
### Pre-requirements
The following tools should be installed on your development machine:
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
* [.NET Core 3.0+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core.
### Install the ABP CLI
[ABP CLI](./CLI.md) is a command line interface that is used to authenticate and automate some tasks for ABP based applications.
> ABP CLI is a free & open source tool for [the ABP framework](https://abp.io/).
First, you need to install the ABP CLI using the following command:
````shell
dotnet tool install -g Volo.Abp.Cli
````
If you've already installed, you can update it using the following command:
````shell
dotnet tool update -g Volo.Abp.Cli
````
## Create a new project
> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document.
### Using the ABP CLI to create a new project
Use the `new` command of the ABP CLI to create a new project:
````shell
abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}}
````
* `-t` argument specifies the [startup template](Startup-Templates/Application.md) name. `app` is the startup template that contains the essential [ABP Modules](Modules/Index.md) pre-installed and configured for you.
{{ if UI == "NG" }}
* `-u` argument specifies the UI framework, `angular` in this case.
{{ if Tiered == "Yes" }}
* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint.
{{ end }}
{{ end }}
{{ if DB == "Mongo" }}
* `-d` argument specifies the database provider, `mongodb` in this case.
{{ end }}
{{ if Tiered == "Yes" && UI != "NG" }}
* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated.
{{ end }}
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.
#### ABP CLI commands & options
[ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates.
## The solution structure
{{ if UI == "MVC" }}
After creating your project, you will have the following solution folders & files:
![](images/solution-files-mvc.png)
You will see the following solution structure when you open the `.sln` file in the Visual Studio:
{{if DB == "Mongo"}}
![vs-default-app-solution-structure](images/vs-app-solution-structure-mongodb.png)
{{else}}
![vs-default-app-solution-structure](images/vs-app-solution-structure{{if Tiered == "Yes"}}-tiered{{end}}.png)
{{end}}
{{ else if UI == "NG" }}
There are three folders in the created solution:
![](images/solution-files-non-mvc.png)
* `angular` folder contains the Angular UI application.
* `aspnet-core` folder contains the backend solution.
* `react-native` folder contains the React Native UI application.
Open the `.sln` (Visual Studio solution) file under the `aspnet-core` folder:
![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png)
{{ end }}
> ###### About the projects in your solution
>
> Your solution may have slightly different structure based on your **UI**, **database** and other preferences.
The solution has a layered structure (based on [Domain Driven Design](./Domain-Driven-Design.md)) and also contains unit & integration test projects.
{{ if DB == "EF" }}
Integration tests projects are properly configured to work with **EF Core** & **SQLite in-memory** database.
{{ else if DB == "Mongo" }}
Integration tests projects are properly configured to work with in-memory **MongoDB** database created per test (used [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library).
{{ end }}
> See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details.
## Create the database
### Database connection string
Check the **connection string** in the `appsettings.json` file under the {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}`.Web` project{{end}}{{else if UI == "NG" }}`.HttpApi.Host` project{{end}}:
{{ if DB == "EF" }}
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
The solution is configured to use **Entity Framework Core** with **MS SQL Server**. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](https://docs.abp.io/en/abp/latest/Entity-Framework-Core) to learn how to switch to another DBMS.
### Apply the migrations
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations.
#### Apply migrations using the DbMigrator
The solution comes with a `.DbMigrator` console application which applies migrations and also seed the initial data. It is useful on development as well as on production environment.
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one.
Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![set-as-startup-project](images/set-as-startup-project.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
![db-migrator-output](images/db-migrator-output.png)
> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database.
#### Using EF Core Update-Database command
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations.
{{ if UI == "MVC" }}
Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**:
{{ else if UI != "MVC" }}
Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**:
{{ end }}
![set-as-startup-project](images/set-as-startup-project.png)
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command:
![package-manager-console-update-database](images/package-manager-console-update-database.png)
This will create a new database based on the configured connection string.
> Using the `.Migrator` tool is the suggested way, because it also seeds the initial data to be able to properly run the web application.
{{ else if DB == "Mongo" }}
````json
"ConnectionStrings": {
"Default": "mongodb://localhost:27017/BookStore"
}
````
The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server.
### Seed initial data
The solution comes with a `.DbMigrator` console application which seeds the initial data. It is useful on development as well as on production environment.
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one.
Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![set-as-startup-project](images/set-as-startup-project.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
![db-migrator-output](images/db-migrator-output.png)
> Initial seed data creates the `admin` user in the database which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database.
{{ end }}
## Run the application
{{ if UI == "MVC" }}
{{ if Tiered == "Yes" }}
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser.
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
You can login, but you cannot enter to the main application here. This is just the authentication server.
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser.
![swagger-ui](images/swagger-ui.png)
This is the API application that is used by the web application.
Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser
![mvc-tiered-app-home](images/bookstore-home.png)
Click to the **login** button which will redirect you to the `Identity Server` to login to the application:
![bookstore-login](images/bookstore-login.png)
{{ else }}
Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser:
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
![bookstore-login](images/bookstore-login.png)
{{ end }}
{{ else if UI != "MVC" }}
#### Running the HTTP API Host (server-side)
{{ if Tiered == "Yes" }}
Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser.
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
You can login, but you cannot enter to the main application here. This is just the authentication server.
{{ end }}
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI:
{{ if Tiered == "No" }}
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
{{ end }}
![swagger-ui](images/swagger-ui.png)
You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI.
> ##### Authorization for the Swagger UI
>
> Most of the HTTP APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too.
{{ end }}
{{ if UI == "NG" }}
#### Running the Angular application (client-side)
Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases)
```bash
yarn
```
Once all node modules are loaded, execute `yarn start` (or `npm start`) command:
```bash
yarn start
```
Wait `Angular CLI` to launch `Webpack` dev-server with `BrowserSync`.
This will take care of compiling your `TypeScript` code, and automatically reloading your browser.
After it finishes, `Angular Live Development Server` will be listening on localhost:4200,
open your web browser and navigate to [localhost:4200](http://localhost:4200/)
![bookstore-login](images/bookstore-login.png)
{{ end }}
Enter **admin** as the username and **1q2w3E*** as the password to login to the application:
![bookstore-home](images/bookstore-home.png)
The application is up and running. You can start developing your application based on this startup template.
#### Mobile Development
ABP platform provide [React Native](https://reactnative.dev/) template to develop mobile applications.
>The solution includes the React Native application in the `react-native` folder as default. If you don't plan to develop a mobile application with React Native, you can ignore this step and delete the `react-native` folder.
The React Native application running on an Android emulator or a physical phone cannot connect to the backend on `localhost`. To fix this problem, it is necessary to run backend on the local IP.
{{ if Tiered == "No"}}
![React Native host project local IP entry](images/rn-host-local-ip.png)
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `SelfUrl` and `Authority` properties with your local IP address.
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address.
{{ else if Tiered == "Yes" }}
![React Native tiered project local IP entry](images/rn-tiered-local-ip.png)
* Open the `appsettings.json` in the `.IdentityServer` folder. Replace the `localhost` address on the `SelfUrl` property with your local IP address.
* Open the `launchSettings.json` in the `.IdentityServer/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address.
* Open the `appsettings.json` in the `.HttpApi.Host` folder. Replace the `localhost` address on the `Authority` property with your local IP address.
* Open the `launchSettings.json` in the `.HttpApi.Host/Properties` folder. Replace the `localhost` address on the `applicationUrl` properties with your local IP address.
{{ end }}
Run the backend as described in the [**Running the HTTP API Host (server-side)**](#running-the-http-api-host-server-side) section.
> React Native application does not trust the auto-generated .NET HTTPS certificate, you should use the HTTP during development.
Go to the `react-native` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work in most cases):
```bash
yarn
```
* Open the `Environment.js` in the `react-native` folder and replace the `localhost` address on the `apiUrl` and `issuer` properties with your local IP address as shown below:
![react native environment local IP](images/rn-environment-local-ip.png)
{{ if Tiered == "Yes" }}
> Make sure that `issuer` matches the running address of the `.IdentityServer` project, `apiUrl` matches the running address of the `.HttpApi.Host` project.
{{else}}
> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` project.
{{ end }}
Once all node modules are loaded, execute `yarn start` (or `npm start`) command:
```bash
yarn start
```
Wait Expo CLI to start. Expo CLI opens the management interface on the `http://localhost:19002/` address.
![expo-interface](images/rn-expo-interface.png)
In the above management interface, you can start the application with an Android emulator, an iOS simulator or a physical phone by the scan the QR code with the [Expo Client](https://expo.io/tools#client).
> See the [Android Studio Emulator](https://docs.expo.io/versions/v36.0.0/workflow/android-studio-emulator/), [iOS Simulator](https://docs.expo.io/versions/v36.0.0/workflow/ios-simulator/) documents on expo.io.
![React Native login screen on iPhone 11](images/rn-login-iphone.png)
Enter **admin** as the username and **1q2w3E*** as the password to login to the application.
The application is up and running. You can continue to develop your application based on this startup template.
> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules.
## What's next?
[Application development tutorial](Tutorials/Part-1.md)

201
docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md

@ -0,0 +1,201 @@
# 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 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 with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
* 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.
* 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).

113
docs/en/How-To/Customize-Login-Page-MVC.md

@ -0,0 +1,113 @@
# 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 thanks to the [Virtual File System](../Virtual-File-System.md).
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
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage
@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).

101
docs/en/How-To/Customize-SignIn-Manager.md

@ -0,0 +1,101 @@
# 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).

9
docs/en/How-To/Index.md

@ -0,0 +1,9 @@
# "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)

2
docs/en/Modules/Docs.md

@ -134,7 +134,7 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<PermissionOptions>(options => Configure<AbpPermissionOptions>(options =>
{ {
options.DefinitionProviders.Add<MyProjectPermissionDefinitionProvider>(); options.DefinitionProviders.Add<MyProjectPermissionDefinitionProvider>();
}); });

8
docs/en/Multi-Tenancy.md

@ -4,7 +4,7 @@ ABP Multi-tenancy module provides base functionality to create multi tenant appl
Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that: Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that:
> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of a software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants. > Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.
### Volo.Abp.MultiTenancy Package ### Volo.Abp.MultiTenancy Package
@ -172,11 +172,11 @@ namespace MyCompany.MyProject
{ {
options.Tenants = new[] options.Tenants = new[]
{ {
new TenantInformation( new TenantConfiguration(
Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id
"tenant1" //Name "tenant1" //Name
), ),
new TenantInformation( new TenantConfiguration(
Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id
"tenant2" //Name "tenant2" //Name
) )
@ -252,7 +252,7 @@ TODO: This package implements ITenantStore using a real database...
#### Tenant Information #### Tenant Information
ITenantStore works with **TenantInformation** class that has several properties for a tenant: ITenantStore works with **TenantConfiguration** class that has several properties for a tenant:
* **Id**: Unique Id of the tenant. * **Id**: Unique Id of the tenant.
* **Name**: Unique name of the tenant. * **Name**: Unique name of the tenant.

15
docs/en/Nightly-Builds.md

@ -24,3 +24,18 @@ Now, you can install preview / nightly packages to your project from Nuget Brows
3. Search a package. You will see prereleases of the package formatted as `(VERSION)-preview(DATE)` (like *v0.16.0-preview20190401* in this sample). 3. Search a package. You will see prereleases of the package formatted as `(VERSION)-preview(DATE)` (like *v0.16.0-preview20190401* in this sample).
4. You can click to the `Install` button to add package to your project. 4. You can click to the `Install` button to add package to your project.
## Install & Uninstall Preview NPM Packages
The latest version of preview NPM packages can be installed by the running below command in the root folder of application:
```bash
abp switch-to-preview
```
If you're using the ABP Framework preview packages, you can switch back to stable version using this command:
```bash
abp switch-to-stable
```
See the [ABP CLI documentation](./CLI.md) for more information.

365
docs/en/Object-Extensions.md

@ -0,0 +1,365 @@
# Object Extensions
ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module.
> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes.
## IHasExtraProperties Interface
This is the interface to make a class extensible. It simply defines a `Dictionary` property:
````csharp
Dictionary<string, object> ExtraProperties { get; }
````
Then you can add or get extra properties using this dictionary.
### Base Classes
`IHasExtraProperties` interface is implemented by several base classes by default:
* Implemented by the `AggregateRoot` class (see [entities](Entities.md)).
* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes.
* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object.
So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually.
### Fundamental Extension Methods
While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties.
#### SetProperty
Used to set the value of an extra property:
````csharp
user.SetProperty("Title", "My Title");
user.SetProperty("IsSuperUser", true);
````
`SetProperty` returns the same object, so you can chain it:
````csharp
user.SetProperty("Title", "My Title")
.SetProperty("IsSuperUser", true);
````
#### GetProperty
Used to read the value of an extra property:
````csharp
var title = user.GetProperty<string>("Title");
if (user.GetProperty<bool>("IsSuperUser"))
{
//...
}
````
* `GetProperty` is a generic method and takes the object type as the generic parameter.
* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc).
##### Non Primitive Property Types
If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`.
#### HasProperty
Used to check if the object has a property set before.
#### RemoveProperty
Used to remove a property from the object. Use this methods instead of setting a `null` value for the property.
### Some Best Practices
Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead;
* Define a constant for your extra property names
* Create extension methods to easily set your extra properties.
Example:
````csharp
public static class IdentityUserExtensions
{
private const string TitlePropertyName = "Title";
public static void SetTitle(this IdentityUser user, string title)
{
user.SetProperty(TitlePropertyName, title);
}
public static string GetTitle(this IdentityUser user)
{
return user.GetProperty<string>(TitlePropertyName);
}
}
````
Then you can easily set or get the `Title` property:
````csharp
user.SetTitle("My Title");
var title = user.GetTitle();
````
## Object Extension Manager
While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes.
Explicitly defining an extra property has some use cases:
* Allows to control how the extra property is handled on object to object mapping (see the section below).
* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md).
> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside.
### AddOrUpdate
`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object.
Example: Define extra properties for the `IdentityUser` entity:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUser>(options =>
{
options.AddOrUpdateProperty<string>("SocialSecurityNumber");
options.AddOrUpdateProperty<bool>("IsSuperUser");
}
);
````
### AddOrUpdateProperty
While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber");
````
Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<string>(
new[]
{
typeof(IdentityUserDto),
typeof(IdentityUserCreateDto),
typeof(IdentityUserUpdateDto)
},
"SocialSecurityNumber"
);
````
### Property Configuration
`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
//Configure options...
});
````
> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document.
The following sections explain the fundamental property configuration options.
#### CheckPairDefinitionOnMapping
Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better.
## Validation
You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation:
1. You can add **data annotation attributes** for a property.
2. You can write an action (code block) to perform a **custom validation**.
Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated.
### Data Annotation Attributes
All of the standard data annotation attributes are valid for extra properties. Example:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"SocialSecurityNumber",
options =>
{
options.ValidationAttributes.Add(new RequiredAttribute());
options.ValidationAttributes.Add(
new StringLengthAttribute(32) {
MinimumLength = 6
}
);
});
````
With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided.
### Custom Validation
If you need, you can add a custom action that is executed to validate the extra properties. Example:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"SocialSecurityNumber",
options =>
{
options.Validators.Add(context =>
{
var socialSecurityNumber = context.Value as string;
if (socialSecurityNumber == null ||
socialSecurityNumber.StartsWith("X"))
{
context.ValidationErrors.Add(
new ValidationResult(
"Invalid social security number: " + socialSecurityNumber,
new[] { "SocialSecurityNumber" }
)
);
}
});
});
````
`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios.
In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUserCreateDto>(objConfig =>
{
//Define two properties with their own validation rules
objConfig.AddOrUpdateProperty<string>("Password", propertyConfig =>
{
propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
});
objConfig.AddOrUpdateProperty<string>("PasswordRepeat", propertyConfig =>
{
propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
});
//Write a common validation logic works on multiple properties
objConfig.Validators.Add(context =>
{
if (context.ValidatingObject.GetProperty<string>("Password") !=
context.ValidatingObject.GetProperty<string>("PasswordRepeat"))
{
context.ValidationErrors.Add(
new ValidationResult(
"Please repeat the same password!",
new[] { "Password", "PasswordRepeat" }
)
);
}
});
});
````
## Object to Object Mapping
Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients.
This section offers some **good practices** to control your extra properties on object mapping.
### MapExtraPropertiesTo
`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage:
````csharp
identityUser.MapExtraPropertiesTo(identityUserDto);
````
`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction.
#### MappingPropertyDefinitionChecks
`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation:
````csharp
identityUser.MapExtraPropertiesTo(
identityUserDto,
MappingPropertyDefinitionChecks.None
);
````
> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too.
If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.CheckPairDefinitionOnMapping = false;
});
````
#### Ignored Properties
You may want to ignore some properties on a specific mapping operation:
````csharp
identityUser.MapExtraPropertiesTo(
identityUserDto,
ignoredProperties: new[] {"MySensitiveProp"}
);
````
Ignored properties are not copied to the target object.
#### AutoMapper Integration
If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above.
You can use the `MapExtraProperties()` method inside your mapping profile.
````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<IdentityUser, IdentityUserDto>()
.MapExtraProperties();
}
}
````
It has the same parameters with the `MapExtraPropertiesTo` method.
## Entity Framework Core Database Mapping
If you're using the EF Core, you can map an extra property to a table field in the database. Example:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
}
);
````
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more.

17
docs/en/Object-To-Object-Mapping.md

@ -145,6 +145,23 @@ options.AddProfile<MyProfile>(validate: true);
> If you have multiple profiles and need to enable validation only for a few of them, first use `AddMaps` without validation, then use `AddProfile` for each profile you want to validate. > If you have multiple profiles and need to enable validation only for a few of them, first use `AddMaps` without validation, then use `AddProfile` for each profile you want to validate.
### Mapping the Object Extensions
[Object extension system](Object-Extensions.md) allows to define extra properties for existing classes. ABP Framework provides a mapping definition extension to properly map extra properties of two objects.
````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<User, UserDto>()
.MapExtraProperties();
}
}
````
It is suggested to use the `MapExtraProperties()` method if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](Object-Extensions.md) for more.
## Advanced Topics ## Advanced Topics
### IObjectMapper<TContext> Interface ### IObjectMapper<TContext> Interface

192
docs/en/Startup-Templates/Application.md

@ -2,11 +2,12 @@
## Introduction ## Introduction
This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below: This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices.
* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider). This document explains **the solution structure** and projects in details. If you want to start quickly, follow the guides below:
* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
* See the [Angular Application Development Tutorial](../Tutorials/Part-1.md?UI=NG) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider). * [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes.
* [The application development tutorial](../Tutorials/Part-1) explains step by step application development.
## How to Start With? ## How to Start With?
@ -123,6 +124,8 @@ Notice that the migration `DbContext` is only used for database migrations and *
* Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application. * Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application.
> This project is available only if you are using EF Core as the database provider. > This project is available only if you are using EF Core as the database provider.
>
> See the [Entity Framework Core Migrations Guide](../Entity-Framework-Core-Migrations.md) to understand this project in details.
#### .DbMigrator Project #### .DbMigrator Project
@ -258,16 +261,183 @@ You should run the application with the given order:
### Angular UI ### Angular UI
If you choose Angular as the UI framework (using the `-u angular` option), the solution is separated into two folders: If you choose `Angular` as the UI framework (using the `-u angular` option), the solution is being separated into three folders:
* `angular` folder contains the Angular UI application, the client-side code.
* `aspnet-core` folder contains the ASP.NET Core solution, the server-side code.
* `react-native` folder contains the React Native UI application, the client-side code for mobile.
The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the `Angular` application consumes it.
Angular application folder structure looks like below:
![angular-folder-structure](../images/angular-folder-structure.png)
Each of ABP Commercial modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder:
```bash
yarn list --pattern abp
```
Angular application module structure:
![Angular template structure diagram](../images/angular-template-structure-diagram.png)
#### AppModule
`AppModule` is the root module of the application. Some of ABP modules and some essential modules imported to the `AppModule`.
ABP Config modules also have imported to `AppModule`  for initially requirements of lazy-loadable ABP modules.
#### AppRoutingModule
There are lazy-loadable ABP modules in the `AppRoutingModule` as routes.
> Paths of ABP Modules should not be changed.
You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages.
```js
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [AuthGuard, PermissionGuard],
data: {
routes: {
name: 'ProjectName::Menu:Dashboard',
order: 2,
iconClass: 'fa fa-dashboard',
requiredPolicy: 'ProjectName.Dashboard.Host'
} as ABP.Route
}
}
```
In the above example;
* If the user is not logged in, AuthGuard blocks access and redirects to the login page.
* PermissionGuard checks the user's permission with `requiredPolicy` property of the `rotues` object. If the user is not authorized to access the page, the 403 page appears.
* `name` property of `routes` is the menu link label. A localization key can be defined .
* `iconClass` property of `routes` object is the menu link icon class.
* `requiredPolicy` property of `routes` object is the required policy key to access the page.
After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu.
#### Shared Module
The modules that may be required for all modules have imported to the `SharedModule`. You should import the `SharedModule` to all modules.
See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document.
#### Environments
The files under the `src/environments` folder has the essential configuration of the application.
#### Home Module
Home module is an example lazy-loadable module that loads on the root address of the application.
#### Styles
The required style files added to `styles` array in the `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time.
#### Testing
You should create your tests in the same folder as the file file you want to test.
See the [testing document](https://angular.io/guide/testing).
#### Depended Packages
* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library.
* [NGXS](https://www.ngxs.io/) is used as state management library.
* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC).
* [Chart.js](https://www.chartjs.org/) is used to create widgets.
* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms.
### React Native
The solution includes the [React Native](https://reactnative.dev/) application in the `react-native` folder as default.
The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the React Native application consumes it.
The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features.
React Native application folder structure as like below:
![react-native-folder-structure](../images/react-native-folder-structure.png)
* `App.js` is bootstrap component of the application.
* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations defined in this file.
* [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder.
* [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the`src/hocs` folder.
* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in the`src/hooks`.
* [Axios interceptors](https://github.com/axios/axios#interceptors) are created in the `src/interceptors` folder.
* Utility functions are exported from `src/utils` folder.
#### Components
Components that can be used on all screens are created in the `src/components` folder. All components have created as a function that able to use [hooks](https://reactjs.org/docs/hooks-intro.html).
#### Screens
![react-native-navigation-structure](../images/react-native-navigation-structure.png)
Screens are created by creating folders that separate their names in the `src/screens` folder. Certain parts of some screens can be split into components.
Each screen is used in a navigator in the `src/navigators` folder.
#### Navigation
[React Navigation](https://reactnavigation.org/) is used as a navigation library. Navigators are created in the `src/navigators`. A [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) navigator and several [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) navigators have created in this folder. See the [above diagram](#screens) for navigation structure.
#### State Management
[Redux](https://redux.js.org/) is used as state management library. [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development.
Actions, reducers, sagas, selectors are created in the `src/store` folder. Store folder as like below:
![react-native-store-folder](../images/react-native-store-folder.png)
* [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file.
* [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store.
* [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store.
* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder.
* [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder.
#### APIs
[Axios](https://github.com/axios/axios) is used as an HTTP client library. An Axios instance has exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls.
#### Theming
[Native Base](https://nativebase.io/) is used as UI components library. Native Base components can customize easily. See the [Native Base customize](https://docs.nativebase.io/Customize.html#Customize) documentation. We followed the same way.
* Native Base theme variables are in the `src/theme/variables` folder.
* Native Base component styles are in the `src/theme/components` folder. These files have been generated with Native Base's `ejectTheme` script.
* Styles of components override with the files under the `src/theme/overrides` folder.
#### Testing
Unit tests will be created.
* `angular` folder contains the Angular UI solution, the client side. See the [Testing Overview](https://reactjs.org/docs/testing.html) document.
* `aspnet-core` folder contains the ASP.NET Core solution, the server side.
Server side is very similar to the solution described above. `.HttpApi.Host` project serves the API, so the Angular application can consume it. #### Depended Libraries
The files under the `angular/src/environments` folder has the essential configuration of the application. * [Native Base](https://nativebase.io/) is used as UI components library.
* [React Navigation](https://reactnavigation.org/) is used as navigation library.
* [Axios](https://github.com/axios/axios) is used as HTTP client library.
* [Redux](https://redux.js.org/) is used as state management library.
* [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development.
* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes.
* [Redux Persist](https://github.com/rt2zz/redux-persist) is used as state persistance.
* [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors.
* [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library.
* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) library allows loading fonts easily.
* [Formik](https://github.com/jaredpalmer/formik) is used to build forms.
* [Yup](https://github.com/jquense/yup) is used for form validations.
## What's Next? ## What's Next?
- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. - [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes.
- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md) to learn how to develop applications using this template. - [The application development tutorial](../Tutorials/Part-1) explains step by step application development.

2
docs/en/Tutorials/Angular/Part-I.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

2
docs/en/Tutorials/Angular/Part-II.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

2
docs/en/Tutorials/Angular/Part-III.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

2
docs/en/Tutorials/AspNetCore-Mvc/Part-I.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

2
docs/en/Tutorials/AspNetCore-Mvc/Part-II.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

2
docs/en/Tutorials/AspNetCore-Mvc/Part-III.md

@ -4,3 +4,5 @@
* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) * [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
* [With Angular UI](../Part-1?UI=NG) * [With Angular UI](../Part-1?UI=NG)
<!-- TODO: this document has been moved, it should be deleted in the future. -->

166
docs/en/Tutorials/Part-1.md

@ -22,7 +22,7 @@ end
### About this tutorial: ### About this tutorial:
In this tutorial series, you will build an ABP Commercial application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used. In this tutorial series, you will build an ABP application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used.
The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts: The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts:
@ -34,14 +34,14 @@ The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts:
### Creating the project ### Creating the project
Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. But first of all, we need to login to the ABP Platform to create a commercial project. Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI.
#### Create the project #### Create the project
By running the below command, it creates a new ABP Commercial project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `{{UI_Value}}`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document.
```bash ```bash
abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none
``` ```
![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) ![Creating project](./images/bookstore-create-project-{{UI_Text}}.png)
@ -61,7 +61,7 @@ After creating the project, you need to apply the initial migrations and create
To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}} To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}}
Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application Further information, see the [running the application section](../Getting-Started?UI={{UI}}#run-the-application).
![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) ![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png)
@ -335,7 +335,7 @@ INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
### Create the application service ### Create the application service
The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects: The next step is to create an [application service](../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces. * `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces.
* `Acme.BookStore.Application` contains the implementations of your application services. * `Acme.BookStore.Application` contains the implementations of your application services.
@ -874,97 +874,52 @@ We'll see **book-list works!** text on the books page:
Run the following command in the terminal to create a new state, named `BooksState`: Run the following command in the terminal to create a new state, named `BooksState`:
![Initial book list page](./images/bookstore-generate-state-books.png)
```bash ```bash
yarn ng generate ngxs-schematic:state books npx @ngxs/cli --name books --directory src/app/books
``` ```
* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state. * This command creates `books.state.ts` and `books.actions.ts` files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
#### Get books data from backend
Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format).
![BookDto properties](./images/bookstore-swagger-book-dto-properties.png) Import the `BooksState` to the `app.module.ts` in the `src/app` folder and then add the `BooksState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
Open the `books.ts` file in the `app\store\models` folder and replace the content as below:
```js ```js
export namespace Books { // ...
export interface State { import { BooksState } from './books/state/books.state'; //<== imported BooksState ==>
books: Response;
}
export interface Response { @NgModule({
items: Book[]; imports: [
totalCount: number; // other imports
}
export interface Book { NgxsModule.forRoot([BooksState]), //<== added BooksState ==>
name: string;
type: BookType;
publishDate: string;
price: number;
lastModificationTime: string;
lastModifierId: string;
creationTime: string;
creatorId: string;
id: string;
}
export enum BookType { //other imports
Undefined, ],
Adventure, // ...
Biography, })
Dystopia, export class AppModule {}
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry,
}
}
``` ```
* Added `Book` interface that represents a book object and `BookType` enum which represents a book category. #### Generate proxies
#### BooksService ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md)
Create a new service, named `BooksService` to perform `HTTP` calls to the server: Run the following command in the `angular` folder:
```bash ```bash
yarn ng generate service books/shared/books abp generate-proxy --module app
``` ```
![service-terminal-output](./images/bookstore-service-terminal-output.png) ![Generate proxy command](./images/generate-proxy-command.png)
Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: The generated files looks like below:
```js ![Generated files](./images/generated-proxies.png)
import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
import { Books } from '../../store/models';
import { Observable } from 'rxjs';
@Injectable({ #### GetBooks Action
providedIn: 'root',
})
export class BooksService {
constructor(private restService: RestService) {}
get(): Observable<Books.Response> {
return this.restService.request<void, Books.Response>({
method: 'GET',
url: '/api/app/book'
});
}
}
```
* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint. Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions).
Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below: Open the `books.actions.ts` file in `app/books/state` folder and replace the content below:
```js ```js
export class GetBooks { export class GetBooks {
@ -974,41 +929,48 @@ export class GetBooks {
#### Implement BooksState #### Implement BooksState
Open the `books.state.ts` file in `app\store\states` folder and replace the content below: Open the `books.state.ts` file in `app/books/state` folder and replace the content below:
```js ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from '../actions/books.actions'; import { GetBooks } from './books.actions';
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
@State<Books.State>({ export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) {} constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((booksResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: booksResponse,
}); });
}), })
); );
} }
} }
``` ```
* We added the book property to BooksStateModel model.
* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. * We added the `GetBooks` action that retrieves the books data via `BooksService` that generated via ABP CLI and patches the state.
* `NGXS` requires to return the observable without subscribing it in the get function. * `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent #### BookListComponent
@ -1017,11 +979,12 @@ Open the `book-list.component.ts` file in `app\books\book-list` folder and repla
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -1030,13 +993,13 @@ import { GetBooks } from '../../store/actions';
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
constructor(private store: Store) { } constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -1044,9 +1007,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
} }
``` ```

533
docs/en/Tutorials/Part-2.md

@ -456,159 +456,76 @@ Run the application and try to delete a book.
In this section, you will learn how to create a new modal dialog form to create a new book. In this section, you will learn how to create a new modal dialog form to create a new book.
#### Type definition
Open `books.ts` file in `app\store\models` folder and replace the content as below:
```js
export namespace Books {
export interface State {
books: Response;
}
export interface Response {
items: Book[];
totalCount: number;
}
export interface Book {
name: string;
type: BookType;
publishDate: string;
price: number;
lastModificationTime: string;
lastModifierId: string;
creationTime: string;
creatorId: string;
id: string;
}
export enum BookType {
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry,
}
//<== added CreateUpdateBookInput interface ==>
export interface CreateUpdateBookInput {
name: string;
type: BookType;
publishDate: string;
price: number;
}
}
```
* We added `CreateUpdateBookInput` interface.
* You can see the properties of this interface from Swagger UI.
* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend.
#### Service method
Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
```js
import { Injectable } from '@angular/core';
import { RestService } from '@abp/ng.core';
import { Books } from '../../store/models';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class BooksService {
constructor(private restService: RestService) {}
get(): Observable<Books.Response> {
return this.restService.request<void, Books.Response>({
method: 'GET',
url: '/api/app/book'
});
}
//<== added create method ==>
create(createBookInput: Books.CreateUpdateBookInput): Observable<Books.Book> {
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({
method: 'POST',
url: '/api/app/book',
body: createBookInput
});
}
}
```
- We added the `create` method to perform an HTTP Post request to the server.
- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used).
#### State definitions #### State definitions
Open `books.action.ts` in `app\store\actions` folder and replace the content as below: Open `books.action.ts` in `books\state` folder and replace the content as below:
```js ```js
import { Books } from '../models'; //<== added this line ==> import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==>
export class GetBooks { export class GetBooks {
static readonly type = '[Books] Get'; static readonly type = '[Books] Get';
} }
//added CreateUpdateBook class // added CreateUpdateBook class
export class CreateUpdateBook { export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book'; static readonly type = '[Books] Create Update Book';
constructor(public payload: Books.CreateUpdateBookInput) { } constructor(public payload: CreateUpdateBookDto) { }
} }
``` ```
* We imported the Books namespace and created the `CreateUpdateBook` action. * We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action.
Open `books.state.ts` file in `app\store\states` and replace the content as below: Open `books.state.ts` file in `books\state` folder and replace the content as below:
```js ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==> import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==>
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<Books.State>({ @State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) { } constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((bookResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: bookResponse,
}); });
}), })
); );
} }
//added CreateUpdateBook action listener // added CreateUpdateBook action listener
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
return this.booksService.create(action.payload); return this.bookService.createByInput(action.payload);
} }
} }
``` ```
* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book. * We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book.
When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`. When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`.
#### Add a modal to BookListComponent #### Add a modal to BookListComponent
@ -688,15 +605,16 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. * `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
* We added `New book` button to the `AbpContentToolbar`. * We added `New book` button to the `AbpContentToolbar`.
Open `book-list.component.` file in `books\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `books\book-list` folder and replace the content as below:
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -705,15 +623,15 @@ import { GetBooks } from '../../store/actions';
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
isModalOpen = false; //<== added this line ==> isModalOpen = false; // <== added this line ==>
constructor(private store: Store) { } constructor(private store: Store) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -721,12 +639,13 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
//added createBook method // added createBook method
createBook() { createBook() {
this.isModalOpen = true; this.isModalOpen = true;
} }
@ -747,12 +666,13 @@ Open `book-list.component.ts` file in `app\books\book-list` folder and replace t
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
@ -761,17 +681,17 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
loading = false; loading = false;
isModalOpen = false; isModalOpen = false;
form: FormGroup; form: FormGroup; // <== added this line ==>
constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==> constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -779,9 +699,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -789,7 +710,7 @@ export class BookListComponent implements OnInit {
this.isModalOpen = true; this.isModalOpen = true;
} }
//added buildForm method // added buildForm method
buildForm() { buildForm() {
this.form = this.fb.group({ this.form = this.fb.group({
name: ['', Validators.required], name: ['', Validators.required],
@ -802,6 +723,7 @@ export class BookListComponent implements OnInit {
``` ```
* We imported `FormGroup, FormBuilder and Validators`. * We imported `FormGroup, FormBuilder and Validators`.
* We added `form: FormGroup` variable.
* We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. * We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book. * We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book.
* The `group` method of `FormBuilder`, `fb` creates a `FormGroup`. * The `group` method of `FormBuilder`, `fb` creates a `FormGroup`.
@ -878,35 +800,34 @@ export class BooksModule { }
* We imported `NgbDatepickerModule` to be able to use the date picker. * We imported `NgbDatepickerModule` to be able to use the date picker.
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==> providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
//added bookTypeArr array //added bookTypeArr array
bookTypeArr = Object.keys(Books.BookType).filter( bookTypeArr = Object.keys(BookType).filter(
bookType => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -915,7 +836,7 @@ export class BookListComponent implements OnInit {
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { } constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -923,9 +844,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -963,35 +885,16 @@ Now, you can open your browser to see the changes:
#### Saving the book #### Saving the book
Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
```html
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
<!--added save button-->
<button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
<i class="fa fa-check mr-1"></i>
{%{{{ 'AbpAccount::Save' | abpLocalization }}}%}
</button>
</ng-template>
```
* This adds a save button to the bottom area of the modal:
![Save button to the modal](./images/bookstore-new-book-form-v2.png)
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==>
import { BooksState } from '../state/books.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -999,16 +902,17 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
bookTypeArr = Object.keys(Books.BookType).filter( //added bookTypeArr array
bookType => typeof this.booksType[bookType] === 'number' bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -1017,7 +921,7 @@ export class BookListComponent implements OnInit {
form: FormGroup; form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) { } constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -1025,9 +929,10 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
createBook() { createBook() {
@ -1060,37 +965,46 @@ export class BookListComponent implements OnInit {
``` ```
* We imported `CreateUpdateBook`. * We imported `CreateUpdateBook`.
* We added `save` method * We added `save` method
### Updating an existing book Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
#### BooksService
Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods. ```html
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
<!--added save button-->
<button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
<i class="fa fa-check mr-1"></i>
{%{{{ 'AbpAccount::Save' | abpLocalization }}}%}
</button>
</ng-template>
```
```js Find the `<form [formGroup]="form">` tag and replace below content:
getById(id: string): Observable<Books.Book> {
return this.restService.request<void, Books.Book>({
method: 'GET',
url: `/api/app/book/${id}`
});
}
update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable<Books.Book> { ```html
return this.restService.request<Books.CreateUpdateBookInput, Books.Book>({ <form [formGroup]="form" (ngSubmit)="save()"> <!-- added the ngSubmit -->
method: 'PUT',
url: `/api/app/book/${id}`,
body: updateBookInput
});
}
``` ```
* We added the `(ngSubmit)="save()"` to `<form>` element to save a new book by pressing the enter.
* We added `abp-button` to the bottom area of the modal to save a new book.
The final modal UI looks like below:
![Save button to the modal](./images/bookstore-new-book-form-v2.png)
### Updating a book
#### CreateUpdateBook action #### CreateUpdateBook action
Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below: Open the `books.actions.ts` in `books\state` folder and replace the content as below:
```js ```js
import { Books } from '../models'; import { CreateUpdateBookDto } from '../../app/shared/models';
export class GetBooks { export class GetBooks {
static readonly type = '[Books] Get'; static readonly type = '[Books] Get';
@ -1098,54 +1012,55 @@ export class GetBooks {
export class CreateUpdateBook { export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book'; static readonly type = '[Books] Create Update Book';
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==> constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==>
} }
``` ```
* We added `id` parameter to the `CreateUpdateBook` action's constructor. * We added `id` parameter to the `CreateUpdateBook` action's constructor.
Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below: Open the `books.state.ts` in `books\state` folder and replace the `save` method as below:
```js ```js
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
if (action.id) { if (action.id) {
return this.booksService.update(action.payload, action.id); return this.bookService.updateByIdAndInput(action.payload, action.id);
} else { } else {
return this.booksService.create(action.payload); return this.bookService.createByInput(action.payload);
} }
} }
``` ```
#### BookListComponent #### BookListComponent
Open `book-list.component.ts` in `app\books\book-list` folder and inject `BooksService` dependency by adding it to the constructor and add a variable named `selectedBook`. Open `book-list.component.ts` in `app\books\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`.
```js ```js
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { BooksState } from '../../store/states';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Books } from '../../store/models'; import { finalize } from 'rxjs/operators';
import { GetBooks, CreateUpdateBook } from '../../store/actions'; import { BookDto, BookType } from '../../app/shared/models';
import { GetBooks, CreateUpdateBook } from '../state/books.actions';
import { BooksState } from '../state/books.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BooksService } from '../shared/books.service'; //<== imported BooksService ==> import { BookService } from '../../app/shared/services'; // <== imported BookService ==>
@Component({ @Component({
selector: 'app-book-list', selector: 'app-book-list',
templateUrl: './book-list.component.html', templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'], styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
}) })
export class BookListComponent implements OnInit { export class BookListComponent implements OnInit {
@Select(BooksState.getBooks) @Select(BooksState.getBooks)
books$: Observable<Books.Book[]>; books$: Observable<BookDto[]>;
booksType = Books.BookType; booksType = BookType;
bookTypeArr = Object.keys(Books.BookType).filter( bookTypeArr = Object.keys(BookType).filter(
bookType => typeof this.booksType[bookType] === 'number' (bookType) => typeof this.booksType[bookType] === 'number'
); );
loading = false; loading = false;
@ -1154,9 +1069,9 @@ export class BookListComponent implements OnInit {
form: FormGroup; form: FormGroup;
selectedBook = {} as Books.Book; //<== declared selectedBook ==> selectedBook = {} as BookDto; // <== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { } constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==>
ngOnInit() { ngOnInit() {
this.get(); this.get();
@ -1164,39 +1079,38 @@ export class BookListComponent implements OnInit {
get() { get() {
this.loading = true; this.loading = true;
this.store.dispatch(new GetBooks()).subscribe(() => { this.store
this.loading = false; .dispatch(new GetBooks())
}); .pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
} }
//<== this method is replaced ==> // <== this method is replaced ==>
createBook() { createBook() {
this.selectedBook = {} as Books.Book; //<== added ==> this.selectedBook = {} as BookDto; // <== added ==>
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
} }
//<== added editBook method ==> // <== added editBook method ==>
editBook(id: string) { editBook(id: string) {
this.booksService.getById(id).subscribe(book => { this.bookService.getById(id).subscribe((book) => {
this.selectedBook = book; this.selectedBook = book;
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
}); });
} }
//<== this method is replaced ==> // <== this method is replaced ==>
buildForm() { buildForm() {
this.form = this.fb.group({ this.form = this.fb.group({
name: [this.selectedBook.name || "", Validators.required], name: [this.selectedBook.name || '', Validators.required],
type: [this.selectedBook.type || null, Validators.required], type: [this.selectedBook.type || null, Validators.required],
publishDate: [ publishDate: [
this.selectedBook.publishDate this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
? new Date(this.selectedBook.publishDate) Validators.required,
: null,
Validators.required
], ],
price: [this.selectedBook.price || null, Validators.required] price: [this.selectedBook.price || null, Validators.required],
}); });
} }
@ -1206,7 +1120,8 @@ export class BookListComponent implements OnInit {
} }
//<== added this.selectedBook.id ==> //<== added this.selectedBook.id ==>
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) this.store
.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
.subscribe(() => { .subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
@ -1216,9 +1131,9 @@ export class BookListComponent implements OnInit {
} }
``` ```
* We imported `BooksService`. * We imported `BookService`.
* We declared a variable named `selectedBook` as `Books.Book`. * We declared a variable named `selectedBook` as `BookDto`.
* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited. * We injected `BookService` to the constructor. `BookService` is being used to retrieve the book data which is being edited.
* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object. * We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data. * We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object. * We replaced the `createBook` method so it sets `selectedBook` to an empty object.
@ -1296,24 +1211,9 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the `<n
### Deleting a book ### Deleting a book
#### BooksService
Open `books.service.ts` in `app\books\shared` folder and add the below `delete` method to delete a book.
```js
delete(id: string): Observable<void> {
return this.restService.request<void, void>({
method: 'DELETE',
url: `/api/app/book/${id}`
});
}
```
* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint.
#### DeleteBook action #### DeleteBook action
Open `books.actions.ts` in `app\store\actions `folder and add an action named `DeleteBook`. Open `books.actions.ts` in `books\state `folder and add an action named `DeleteBook`.
```js ```js
export class DeleteBook { export class DeleteBook {
@ -1322,51 +1222,58 @@ export class DeleteBook {
} }
``` ```
Open the `books.state.ts` in `app\store\states` folder and replace the content as below: Open the `books.state.ts` in `books\state` folder and replace the content as below:
```js ```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store'; import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==> import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==>
import { Books } from '../models/books'; import { BookService } from '../../app/shared/services';
import { BooksService } from '../../books/shared/books.service';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../../app/shared/models';
@State<Books.State>({ export class BooksStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState', name: 'BooksState',
defaults: { books: {} } as Books.State, defaults: { book: {} } as BooksStateModel,
}) })
@Injectable()
export class BooksState { export class BooksState {
@Selector() @Selector()
static getBooks(state: Books.State) { static getBooks(state: BooksStateModel) {
return state.books.items || []; return state.book.items || [];
} }
constructor(private booksService: BooksService) { } constructor(private bookService: BookService) {}
@Action(GetBooks) @Action(GetBooks)
get(ctx: StateContext<Books.State>) { get(ctx: StateContext<BooksStateModel>) {
return this.booksService.get().pipe( return this.bookService.getListByInput().pipe(
tap(booksResponse => { tap((booksResponse) => {
ctx.patchState({ ctx.patchState({
books: booksResponse, book: booksResponse,
}); });
}), })
); );
} }
@Action(CreateUpdateBook) @Action(CreateUpdateBook)
save(ctx: StateContext<Books.State>, action: CreateUpdateBook) { save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
if (action.id) { if (action.id) {
return this.booksService.update(action.payload, action.id); return this.bookService.updateByIdAndInput(action.payload, action.id);
} else { } else {
return this.booksService.create(action.payload); return this.bookService.createByInput(action.payload);
} }
} }
//<== added DeleteBook ==> // <== added DeleteBook action listener ==>
@Action(DeleteBook) @Action(DeleteBook)
delete(ctx: StateContext<Books.State>, action: DeleteBook) { delete(ctx: StateContext<BooksStateModel>, action: DeleteBook) {
return this.booksService.delete(action.id); return this.bookService.deleteById(action.id);
} }
} }
``` ```
@ -1375,27 +1282,8 @@ export class BooksState {
- We added `DeleteBook` action listener to the end of the file. - We added `DeleteBook` action listener to the end of the file.
#### Add a delete button
#### Delete confirmation popup
Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below:
```html
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id, data.name)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>
```
The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png)
#### Delete confirmation dialog
Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`.
@ -1406,26 +1294,29 @@ import { ConfirmationService } from '@abp/ng.theme.shared';
//... //...
constructor( constructor(
private store: Store, private fb: FormBuilder, private store: Store,
private booksService: BooksService, private fb: FormBuilder,
private confirmationService: ConfirmationService // <== added this line ==> private bookService: BookService,
private confirmation: ConfirmationService // <== added this line ==>
) { } ) { }
``` ```
* We imported `ConfirmationService`. * We imported `ConfirmationService`.
* We injected `ConfirmationService` to the constructor. * We injected `ConfirmationService` to the constructor.
See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)
In the `book-list.component.ts` add a delete method : In the `book-list.component.ts` add a delete method :
```js ```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==> import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==> import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
//... //...
delete(id: string, name: string) { delete(id: string) {
this.confirmationService this.confirmation
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
.subscribe(status => { .subscribe(status => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
@ -1435,10 +1326,30 @@ delete(id: string, name: string) {
} }
``` ```
The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) ![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
#### Add a delete button
Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below:
```html
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>
```
The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png)
{{end}} {{end}}
### Next Part ### Next Part

4
docs/en/Tutorials/Part-3.md

@ -96,9 +96,9 @@ namespace Acme.BookStore
```` ````
* `IRepository<Book, Guid>` is injected and used it in the `SeedAsync` to create two book entities as the test data. * `IRepository<Book, Guid>` is injected and used it in the `SeedAsync` to create two book entities as the test data.
* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](../Guid-Generation.md).
### Testing the application service BookAppService ### Testing the application service BookAppService
* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation).
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:

BIN
docs/en/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 91 KiB

BIN
docs/en/Tutorials/images/generate-proxy-command.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
docs/en/Tutorials/images/generated-proxies.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

77
docs/en/UI/Angular/Component-Replacement.md

@ -1,25 +1,28 @@
# Component Replacement ## Component Replacement
You can replace some ABP components with your custom components. You can replace some ABP components with your custom components.
The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_. The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_.
## How to Replace a Component ### How to Replace a Component
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//...
export class AppComponent { export class AppComponent {
constructor(..., private store: Store) {} constructor(..., private store: Store) {} // injected Store
ngOnInit() { ngOnInit() {
this.store.dispatch( this.store.dispatch(
new AddReplaceableComponent({ new AddReplaceableComponent({
component: YourNewRoleComponent, component: YourNewRoleComponent,
key: 'Identity.RolesComponent', key: eIdentityComponents.Roles,
}), }),
); );
//... //...
@ -29,23 +32,53 @@ export class AppComponent {
![Example Usage](./images/component-replacement.gif) ![Example Usage](./images/component-replacement.gif)
## Available Replaceable Components
### How to Replace a Layout
| Component key | Description |
| -------------------------------------------------- | --------------------------------------------- | Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced with the same way.
| Account.LoginComponent | Login page |
| Account.RegisterComponent | Register page | > A layout component template should contain `<router-outlet></router-outlet>` element.
| Account.ManageProfileComponent | Manage Profile page |
| Account.AuthWrapperComponent | This component wraps register and login pages | The below example describes how to replace the `ApplicationLayoutComponent`:
| Account.ChangePasswordComponent | Change password form |
| Account.PersonalSettingsComponent | Personal settings form | Run the following command to generate a layout in `angular` folder:
| Account.TenantBoxComponentInputs | Tenant changing box |
| FeatureManagement.FeatureManagementComponent | Features modal | ```bash
| Identity.UsersComponent | Users page | yarn ng generate component shared/my-application-layout --export --entryComponent
| Identity.RolesComponent | Roles page |
| PermissionManagement.PermissionManagementComponent | Permissions modal | # You don't need the --entryComponent option in Angular 9
| SettingManagement.SettingManagementComponent | Setting Management page | ```
| TenantManagement.TenantsComponent | Tenants page |
Add the following code in your layout template (`my-layout.component.html`) where you want the page to be loaded.
```html
<router-outlet></router-outlet>
```
Open the `app.component.ts` and add the below content:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
import { Store } from '@ngxs/store'; // imported Store
//...
export class AppComponent {
constructor(..., private store: Store) {} // injected Store
ngOnInit() {
// added below content
this.store.dispatch(
new AddReplaceableComponent({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
}),
);
//...
}
}
```
## What's Next? ## What's Next?

14
docs/en/UI/Angular/Config-State.md

@ -236,7 +236,7 @@ const newRoute: ABP.Route = {
path: "page", path: "page",
invisible: false, invisible: false,
order: 2, order: 2,
requiredPolicy: "MyProjectName::MyNewPage" requiredPolicy: "MyProjectName.MyNewPage"
}; };
this.config.dispatchAddRoute(newRoute); this.config.dispatchAddRoute(newRoute);
@ -248,23 +248,25 @@ The `newRoute` will be placed as at root level, i.e. without any parent routes a
If you want **to add a child route, you can do this:** If you want **to add a child route, you can do this:**
```js ```js
import { eIdentityRouteNames } from '@abp/ng.identity';
// this.config is instance of ConfigStateService // this.config is instance of ConfigStateService
const newRoute: ABP.Route = { const newRoute: ABP.Route = {
parentName: "AbpAccount::Login", parentName: eIdentityRouteNames.IdentityManagement,
name: "My New Page", name: "My New Page",
iconClass: "fa fa-dashboard", iconClass: "fa fa-dashboard",
path: "page", path: "page",
invisible: false, invisible: false,
order: 2, order: 2,
requiredPolicy: "MyProjectName::MyNewPage" requiredPolicy: "MyProjectName.MyNewPage"
}; };
this.config.dispatchAddRoute(newRoute); this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete // returns a state stream which emits after dispatch action is complete
``` ```
The `newRoute` will then be placed as a child of the parent route named `'AbpAccount::Login'` and its url will be set as `'/account/login/page'`. The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`.
#### Route Configuration Properties #### Route Configuration Properties
@ -288,3 +290,7 @@ Note that **you do not have to call this method at application initiation**, bec
#### Environment Properties #### Environment Properties
Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13).
## What's Next?
- [Modifying the Menu](./Modifying-the-Menu.md)

185
docs/en/UI/Angular/Confirmation-Service.md

@ -0,0 +1,185 @@
# Confirmation Popup
You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project.
## Getting Started
You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ConfirmationService } from '@abp/ng.theme.shared';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private confirmation: ConfirmationService) {}
}
```
## Usage
You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationService` to display a confirmation popup.
### How to Display a Confirmation Popup
```js
const confirmationStatus$ = this.confirmation.success('Message', 'Title');
```
- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`.
- `success`, `warn`, `error`, and `info` methods return an [RxJS Subject](https://rxjs-dev.firebaseapp.com/guide/subject) to listen to confirmation popup closing event. The type of event value is [`Confirmation.Status`](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts#L24) that is an enum.
### How to Listen Closing Event
You can subscribe to the confirmation closing event like below:
```js
import { Confirmation, ConfirmationService } from '@abp/ng.theme.shared';
constructor(private confirmation: ConfirmationService) {}
this.confirmation
.warn('::WillBeDeleted', { key: '::AreYouSure', defaultValue: 'Are you sure?' })
.subscribe((status: Confirmation.Status) => {
// your code here
});
```
- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md)
- `Confirmation.Status` is an enum and has three properties;
- `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button.
- `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button.
- `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape.
If you are not interested in the confirmation status, you do not have to subscribe to the returned observable:
```js
this.confirmation.error('You are not authorized.', 'Error');
```
### How to Display a Confirmation Popup With Given Options
Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods:
```js
const options: Partial<Confirmation.Options> = {
hideCancelBtn: false,
hideYesBtn: false,
cancelText: 'Close',
yesText: 'Confirm',
messageLocalizationParams: ['Demo'],
titleLocalizationParams: [],
};
this.confirmation.warn(
'AbpIdentity::RoleDeletionConfirmationMessage',
'Are you sure?',
options,
);
```
- `hideCancelBtn` option hides the cancellation button when `true`. Default value is `false`
- `hideYesBtn` option hides the confirmation button when `true`. Default value is `false`
- `cancelText` is the text of the cancellation button. A localization key or localization object can be passed. Default value is `AbpUi::Cancel`
- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`
- `messageLocalizationParams` is the interpolation parameters for the localization of the message.
- `titleLocalizationParams` is the interpolation parameters for the localization of the title.
With the options above, the confirmation popup looks like this:
![confirmation](./images/confirmation.png)
You are able to pass in an HTML string as title, message, or button texts. Here is an example:
```js
const options: Partial<Confirmation.Options> = {
yesText: '<i class="fa fa-trash mr-1"></i>Yes, delete it',
};
this.confirmation.warn(
`
<strong>Role Demo</strong> will be <strong>deleted</strong>
<br>
Do you confirm that?
`,
'<span class="my-custom-title">Are you sure?</span>',
options,
);
```
Since the values are HTML now, localization should be handled manually. Check out the [LocalizationService](./Localization#using-the-localization-service) to see how you can accomplish that.
> Please note that all strings will be sanitized by Angular and not every HTML string will work. Only values that are considered as "safe" by Angular will be displayed.
### How to Remove a Confirmation Popup
The open confirmation popup can be removed manually via the `clear` method:
```js
this.confirmation.clear();
```
## API
### success
```js
success(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Confirmation.Status>
```
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts)
### warn
```js
warn(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Confirmation.Status>
```
### error
```js
error(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Confirmation.Status>
```
### info
```js
info(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Confirmation.Status>
```
### clear
```js
clear(
status: Confirmation.Status = Confirmation.Status.dismiss
): void
```
- `status` parameter is the value of the confirmation closing event.
## What's Next?
- [Toast Overlay](./Toaster-Service.md)

101
docs/en/UI/Angular/Container-Strategy.md

@ -0,0 +1,101 @@
# ContainerStrategy
`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected.
## API
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**.
### constructor
```js
constructor(
public containerRef: ViewContainerRef,
private index?: number, // works only in InsertIntoContainerStrategy
)
```
- `containerRef` is the `ViewContainerRef` that will be used when projecting the content.
### getIndex
```js
getIndex(): number
```
This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`.
### prepare
```js
prepare(): void
```
This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop).
## ClearContainerStrategy
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**.
## InsertIntoContainerStrategy
`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**.
## Predefined Container Strategies
Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant.
### Clear
```js
CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef)
```
Clears given container before content projection.
### Append
```js
CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef)
```
Projected content will be appended to the container.
### Prepend
```js
CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef)
```
Projected content will be prepended to the container.
### Insert
```js
CONTAINER_STRATEGY.Insert(
containerRef: ViewContainerRef,
index: number,
)
```
Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`).
## See Also
- [ProjectionStrategy](./Projection-Strategy.md)

78
docs/en/UI/Angular/Content-Projection-Service.md

@ -0,0 +1,78 @@
# Content Projection
You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way.
## Getting Started
You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ContentProjectionService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private contentProjectionService: ContentProjectionService) {}
}
```
## Usage
You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project.
### How to Project Components to Root Level
If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context.
```js
const strategy = PROJECTION_STRATEGY.AppendComponentToBody(
SomeOverlayComponent,
{ someOverlayProp: "SOME_VALUE" }
);
const componentRef = this.contentProjectionService.projectContent(strategy);
```
In the example above, `SomeOverlayComponent` component will placed at the **end** of `<body>` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`.
> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance.
### How to Project Components and Templates into a Container
If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context.
```js
const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer(
SomeComponent,
viewContainerRefOfTarget,
{ someProp: "SOME_VALUE" }
);
const componentRef = this.contentProjectionService.projectContent(strategy);
```
In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`.
> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary.
Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy.
## API
### projectContent
```js
projectContent<T extends Type<any> | TemplateRef<any>>(
projectionStrategy: ProjectionStrategy<T>,
injector = this.injector,
): ComponentRef<C> | EmbeddedViewRef<C>
```
- `projectionStrategy` parameter is the primary focus here and is explained above.
- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`.
## What's Next?
- [TrackByService](./Track-By-Service.md)

74
docs/en/UI/Angular/Content-Security-Strategy.md

@ -0,0 +1,74 @@
# ContentSecurityStrategy
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
## API
### constructor
```js
constructor(public nonce?: string)
```
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives.
### applyCSP
```js
applyCSP(element: HTMLScriptElement | HTMLStyleElement): void
```
This method maps the aforementioned properties to the given `element`.
## LooseContentSecurityPolicy
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it.
## NoContentSecurityPolicy
`NoContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative.
## Predefined Content Security Strategies
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant.
### Loose
```js
CONTENT_SECURITY_STRATEGY.Loose(nonce: string)
```
`nonce` will be set.
### None
```js
CONTENT_SECURITY_STRATEGY.None()
```
Nothing will be done.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)
- [ContentStrategy](./Content-Strategy.md)

95
docs/en/UI/Angular/Content-Strategy.md

@ -0,0 +1,95 @@
# ContentStrategy
`ContentStrategy` is an abstract class exposed by @abp/ng.core package. It helps you create inline scripts or styles.
## API
### constructor
```js
constructor(
public content: string,
protected domStrategy?: DomStrategy,
protected contentSecurityStrategy?: ContentSecurityStrategy
)
```
- `content` is set to `<script>` and `<style>` elements as `textContent` property.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage.
### createElement
```js
createElement(): HTMLScriptElement | HTMLStyleElement
```
This method creates and returns a `<script>` or `<style>` element with `content` set as `textContent`.
### insertElement
```js
insertElement(): void
```
This method creates and inserts a `<script>` or `<style>` element.
## ScriptContentStrategy
`ScriptContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<script>` element to the DOM**.
## StyleContentStrategy
`StyleContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<style>` element to the DOM**.
## Predefined Content Strategies
Predefined content strategies are accessible via `CONTENT_STRATEGY` constant.
### AppendScriptToBody
```js
CONTENT_STRATEGY.AppendScriptToBody(content: string)
```
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document.
### AppendScriptToHead
```js
CONTENT_STRATEGY.AppendScriptToHead(content: string)
```
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document.
### AppendStyleToHead
```js
CONTENT_STRATEGY.AppendStyleToHead(content: string)
```
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document.
### PrependStyleToHead
```js
CONTENT_STRATEGY.PrependStyleToHead(content: string)
```
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)

117
docs/en/UI/Angular/Context-Strategy.md

@ -0,0 +1,117 @@
# ContextStrategy
`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context.
## ComponentContextStrategy
`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**.
### constructor
```js
constructor(public context: Partial<InferredInstanceOf<T>>) {}
```
- `T` refers to component type here, i.e. `Type<C>`.
- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape.
- `context` will be mapped to properties of the projected component.
### setContext
```js
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>>
```
This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping.
## TemplateContextStrategy
`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**.
### constructor
```js
constructor(public context: Partial<InferredContextOf<T>>) {}
```
- `T` refers to template context type here, i.e. `TemplateRef<C>`.
- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape.
- `context` will be mapped to properties of the projected template.
### setContext
```js
setContext(): Partial<InferredContextOf<T>>
```
This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method.
## NoContextStrategy
`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**.
### constructor
```js
constructor()
```
Unlike other context strategies, `NoContextStrategy` contructor takes no parameters.
### setContext
```js
setContext(): undefined
```
Since there is no context, this method gets no parameters and will return `undefined`.
## Predefined Context Strategies
Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant.
### None
```js
CONTEXT_STRATEGY.None()
```
This strategy will not pass any context to the projected content.
### Component
```js
CONTEXT_STRATEGY.Component(context: Partial<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected component.
### Template
```js
CONTEXT_STRATEGY.Template(context: Partial<InferredContextOf<T>>)
```
This strategy will help you pass the given context to the projected template.
## See Also
- [ProjectionStrategy](./Projection-Strategy.md)

60
docs/en/UI/Angular/Cross-Origin-Strategy.md

@ -0,0 +1,60 @@
# CrossOriginStrategy
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor
```js
constructor(
public crossorigin: 'anonymous' | 'use-credentials',
public integrity?: string
)
```
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin).
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity).
### setCrossOrigin
```js
setCrossOrigin(element: HTMLElement): void
```
This method maps the aforementioned properties to the given `element`.
## Predefined Cross-Origin Strategies
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant.
### Anonymous
```js
CROSS_ORIGIN_STRATEGY.Anonymous(integrity?: string)
```
`crossorigin` will be set as `"anonymous"` and `integrity` is optional.
### UseCredentials
```js
CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string)
```
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)

4
docs/en/UI/Angular/Custom-Setting-Page.md

@ -40,3 +40,7 @@ ngOnInit() {
Navigate to `/setting-management` route to see the changes: Navigate to `/setting-management` route to see the changes:
![Custom Settings Tab](./images/custom-settings.png) ![Custom Settings Tab](./images/custom-settings.png)
## What's Next?
- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md)

140
docs/en/UI/Angular/Dom-Insertion-Service.md

@ -0,0 +1,140 @@
# Dom Insertion (of Scripts and Styles)
You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way.
## Getting Started
You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { DomInsertionService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
}
```
## Usage
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. There is also the `projectContent` method for dynamically rendering components and templates.
### How to Insert Scripts
The first parameter of `insertContent` method expects a `ContentStrategy`. If you pass a `ScriptContentStrategy` instance, the `DomInsertionService` will create a `<script>` element with given `content` and place it in the designated DOM position.
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
const scriptElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendScriptToBody('alert()')
);
}
}
```
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>` and `scriptElement` will be an `HTMLScriptElement`.
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method.
### How to Insert Styles
If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position.
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
const styleElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')
);
}
}
```
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>` and `styleElement` will be an `HTMLStyleElement`.
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method.
### How to Remove Inserted Scripts & Styles
If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element.
```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
private styleElement: HTMLStyleElement;
constructor(private domInsertionService: DomInsertionService) {}
ngOnInit() {
this.styleElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')
);
}
ngOnDestroy() {
this.domInsertionService.removeContent(this.styleElement);
}
}
```
In the example above, `<style>body {margin: 0;}</style>` element **will be removed** from `<head>` when the component is destroyed.
## API
### insertContent
```js
insertContent<T extends HTMLScriptElement | HTMLStyleElement>(
contentStrategy: ContentStrategy<T>,
): T
```
- `contentStrategy` parameter is the primary focus here and is explained above.
- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy.
### removeContent
```js
removeContent(element: HTMLScriptElement | HTMLStyleElement): void
```
- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method.
### has
```js
has(content: string): boolean
```
The `has` method returns a boolean value that indicates the given content has already been added to the DOM or not.
- `content` parameter is the content of the inserted `HTMLScriptElement` or `HTMLStyleElement` element.
## What's Next?
- [ContentProjectionService](./Content-Projection-Service.md)

90
docs/en/UI/Angular/Dom-Strategy.md

@ -0,0 +1,90 @@
# DomStrategy
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor
```js
constructor(
public target?: HTMLElement,
public position?: InsertPosition
)
```
- `target` is an HTMLElement (_default: document.head_).
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_).
### insertElement
```js
insertElement(element: HTMLElement): void
```
This method inserts given `element` to `target` based on the `position`.
## Predefined Dom Strategies
Predefined dom strategies are accessible via `DOM_STRATEGY` constant.
### AppendToBody
```js
DOM_STRATEGY.AppendToBody()
```
`insertElement` will place the given `element` at the end of `<body>`.
### AppendToHead
```js
DOM_STRATEGY.AppendToHead()
```
`insertElement` will place the given `element` at the end of `<head>`.
### PrependToHead
```js
DOM_STRATEGY.PrependToHead()
```
`insertElement` will place the given `element` at the beginning of `<head>`.
### AfterElement
```js
DOM_STRATEGY.AfterElement(target: HTMLElement)
```
`insertElement` will place the given `element` after (as a sibling to) the `target`.
### BeforeElement
```js
DOM_STRATEGY.BeforeElement(target: HTMLElement)
```
`insertElement` will place the given `element` before (as a sibling to) the `target`.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)
- [LazyLoadService](./Lazy-Load-Service.md)
- [LoadingStrategy](./Loading-Strategy.md)
- [ContentStrategy](./Content-Strategy.md)
- [ProjectionStrategy](./Projection-Strategy.md)

209
docs/en/UI/Angular/HTTP-Requests.md

@ -0,0 +1,209 @@
# How to Make HTTP Requests
## About HttpClient
Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project.
However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned.
```js
getConfig() {
this.http.get(this.configUrl).subscribe(
config => this.updateConfig(config),
error => {
// Handle error here
},
);
}
```
Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable.
An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details.
## RestService
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult.
### Getting Started with RestService
In order to use the `RestService`, you must inject it in your class as a dependency.
```js
import { RestService } from '@abp/ng.core';
@Injectable({
/* class metadata here */
})
class DemoService {
constructor(private rest: RestService) {}
}
```
You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**.
### How to Make a Request with RestService
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example:
```js
getFoo(id: number) {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, FooResponse>(request);
}
```
The `request` method always returns an `Observable<T>`. Therefore you can do the following wherever you use `getFoo` method:
```js
doSomethingWithFoo(id: number) {
this.demoService.getFoo(id).subscribe(
foo => {
// Do something with foo.
}
)
}
```
**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error.
As you see, `request` method gets a request options object with `Rest.Request<T>` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one:
```js
postFoo(body: Foo) {
const request: Rest.Request<Foo> = {
method: 'POST',
url: '/api/some/path/to/foo',
body
};
return this.rest.request<Foo, FooResponse>(request);
}
```
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular.
### How to Disable Default Error Handler of RestService
The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself:
```js
deleteFoo(id: number) {
const request: Rest.Request<null> = {
method: 'DELETE',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, void>(request, { skipHandleError: true });
}
```
`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription.
```js
removeFooFromList(id: number) {
this.demoService.deleteFoo(id).subscribe(
foo => {
// Do something with foo.
},
error => {
// Do something with error.
}
)
}
```
### How to Get a Specific API Endpoint From Application Config
Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration.
```js
putFoo(body: Foo, id: string) {
const request: Rest.Request<Foo> = {
method: 'PUT',
url: '/' + id,
body
};
return this.rest.request<Foo, void>(request, {apiName: 'foo'});
}
```
`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows:
```js
// environment.ts
export const environment = {
apis: {
default: {
url: 'https://localhost:44305',
},
foo: {
url: 'https://localhost:44305/api/some/path/to/foo',
},
},
/* rest of the environment variables here */
}
```
### How to Observe Response Object or HTTP Events Instead of Body
`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object.
```js
getSomeCustomHeaderValue() {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/that/sends/some-custom-header',
};
return this.rest.request<null, HttpResponse<any>>(
request,
{observe: Rest.Observe.Response},
).pipe(
map(response => response.headers.get('Some-Custom-Header'))
);
}
```
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10).
## What's Next?
* [Localization](./Localization.md)

213
docs/en/UI/Angular/Lazy-Load-Service.md

@ -0,0 +1,213 @@
# How to Lazy Load Scripts and Styles
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way.
## Getting Started
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { LazyLoadService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
}
```
## Usage
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource.
### How to Load Scripts
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="libraryLoaded$ | async"></some-component>
`
})
class DemoComponent {
libraryLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The Scripts will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### How to Load Styles
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="stylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
stylesLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### Advanced Usage
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example:
```js
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh',
);
const loadingStrategy = new StyleLoadingStrategy(
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css',
domStrategy,
crossOriginStrategy,
);
this.lazyLoad.load(loadingStrategy, 1, 2000);
```
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails.
A common usecase is **loading multiple scripts and/or styles before using a feature**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { frokJoin } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
private stylesLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'),
),
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'),
),
);
private scriptsLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'),
),
);
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available.
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**.
Another frequent usecase is **loading dependent scripts in order**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { concat } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsLoaded$ | async"></some-component>
`
})
class DemoComponent {
scriptsLoaded$ = concat(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'),
),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded.
## API
### loaded
```js
loaded: Set<string>
```
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
### load
```js
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>
```
- `strategy` parameter is the primary focus here and is explained above.
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_).
- `retryDelay` defines how much delay there will be between retries (_default: 1000_).
## What's Next?
- [DomInsertionService](./Dom-Insertion-Service.md)

110
docs/en/UI/Angular/Loading-Strategy.md

@ -0,0 +1,110 @@
# LoadingStrategy
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work.
## API
### constructor
```js
constructor(
public path: string,
protected domStrategy?: DomStrategy,
protected crossOriginStrategy?: CrossOriginStrategy
)
```
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage.
### createElement
```js
createElement(): HTMLScriptElement | HTMLLinkElement
```
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`.
### createStream
```js
createStream(): Observable<Event>
```
This method creates and returns an observable stream that emits on success and throws on error.
## ScriptLoadingStrategy
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**.
## StyleLoadingStrategy
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**.
## Predefined Loading Strategies
Predefined loading strategies are accessible via `LOADING_STRATEGY` constant.
### AppendAnonymousScriptToHead
```js
LOADING_STRATEGY.AppendAnonymousScriptToHead(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousScriptToHead
```js
LOADING_STRATEGY.PrependAnonymousScriptToHead(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document.
### AppendAnonymousScriptToBody
```js
LOADING_STRATEGY.AppendAnonymousScriptToBody(src: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document.
### AppendAnonymousStyleToHead
```js
LOADING_STRATEGY.AppendAnonymousStyleToHead(href: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousStyleToHead
```js
LOADING_STRATEGY.PrependAnonymousStyleToHead(href: string, integrity?: string)
```
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document.
## See Also
- [LazyLoadService](./Lazy-Load-Service.md)

6
docs/en/UI/Angular/Localization.md

@ -133,4 +133,8 @@ Localization resources are stored in the `localization` property of `ConfigState
## See Also ## See Also
* [Localization in ASP.NET Core](../../Localization.md) * [Localization in ASP.NET Core](../../Localization.md)
## What's Next?
* [Permission Management](./Permission-Management.md)

199
docs/en/UI/Angular/Modifying-the-Menu.md

@ -0,0 +1,199 @@
# Modifying the Menu
The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic package. There are several methods for modifying the menu elements. This document covers these methods. If you would like to replace the menu completely, please refer to [Component Replacement documentation](./Component-Replacement.md) and learn how to replace a layout.
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.-->
## How to Add a Logo
The `logoUrl` property in the environment variables is the url of the logo.
You can add your logo to `src/assets` folder and set the `logoUrl` as shown below:
```js
export const environment = {
// other configurations
application: {
name: 'MyProjectName',
logoUrl: 'assets/logo.png',
},
// other configurations
};
```
## How to Add a Navigation Element
### Via `routes` Property in `AppRoutingModule`
You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `ConfigState`. `ApplicationLayoutComponent` gets routes from store and displays them on the menu.
You can add the `routes` property like below:
```js
{
path: 'your-path',
data: {
routes: {
name: 'Your navigation',
order: 3,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
children: [
{
path: 'child',
name: 'Your child navigation',
order: 1,
requiredPolicy: 'permission key here',
},
],
} as ABP.Route, // can be imported from @abp/ng.core
}
}
```
- `name` is the label of the navigation element. A localization key or a localization object can be passed.
- `order` is the order of the navigation element.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property.
After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## Via ConfigState
The `dispatchAddRoute` method of `ConfigStateService` adds a new navigation element to the menu.
```js
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 2,
requiredPolicy: 'MyProjectName.MyNewPage',
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
The `newRoute` will be placed as at root level, i.e. without any parent routes, and its url will be stored as `'/path'`.
If you want **to add a child route, you can do this:**
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRoute: ABP.Route = {
parentName: eIdentityRouteNames.IdentityManagement,
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 3,
requiredPolicy: 'MyProjectName.MyNewPage'
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
The `newRoute` will then be placed as a child of the parent route named `eIdentityRouteNames.IdentityManagement` and its url will be set as `'/identity/page'`.
The new route will be added like below:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png)
## How to Patch a Navigation Element
The `dispatchPatchRouteByName` method finds a route by its name and replaces its configuration in the store with the new configuration passed as the second parameter.
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration,
order: 0,
children: [
{
name: 'Dashboard',
path: 'dashboard',
},
],
};
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig);
// returns a state stream which emits after dispatch action is complete
```
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
* Added an icon.
* Specified the order.
* Added a child route named _Dashboard_.
After the patch above, navigation elements looks like below:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)
## How to Add an Element to Right Part of the Menu
The right part elements are stored in the `LayoutState` that is in the @abp/ng.theme.basic package.
The `dispatchAddNavigationElement` method of the `LayoutStateService` adds an element to the right part of the menu.
You can insert an element by adding your template to `app.component` and calling the `dispatchAddNavigationElement` method:
```js
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line
@Component({
selector: 'app-root',
template: `
<!-- Added below content -->
<ng-template #search
><input type="search" placeholder="Search" class="bg-transparent border-0"
/></ng-template>
`,
})
export class AppComponent {
// Added ViewChild
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>;
constructor(private layout: LayoutStateService) {} // injected LayoutStateService
// Added ngAfterViewInit
ngAfterViewInit() {
const newElement = {
name: 'Search',
element: this.searchElementRef,
order: 1,
} as Layout.NavigationElement;
this.layout.dispatchAddNavigationElement(newElement);
}
}
```
This inserts a search input to the menu. The final UI looks like below:
![navigation-menu-search-input](./images/navigation-menu-search-input.png)
## How to Remove an Element From Right Part of the Menu
TODO
## What's Next
* [Component Replacement](./Component-Replacement.md)

2
docs/en/UI/Angular/Permission-Management.md

@ -76,4 +76,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`.
## What's Next? ## What's Next?
* [Component Replacement](./Component-Replacement.md) - [Confirmation Popup](./Confirmation-Service.md)

200
docs/en/UI/Angular/Projection-Strategy.md

@ -0,0 +1,200 @@
# ProjectionStrategy
`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work.
## ComponentProjectionStrategy
`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**.
### constructor
```js
constructor(
component: T,
private containerStrategy: ContainerStrategy,
private contextStrategy?: ContextStrategy,
)
```
- `component` is class of the component you would like to project.
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component.
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_)
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage.
### injectContent
```js
injectContent(injector: Injector): ComponentRef<T>
```
This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on.
## RootComponentProjectionStrategy
`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to `<body>`.
### constructor
```js
constructor(
component: T,
private contextStrategy?: ContextStrategy,
private domStrategy?: DomStrategy,
)
```
- `component` is class of the component you would like to project.
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_)
- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_)
Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage.
### injectContent
```js
injectContent(injector: Injector): ComponentRef<T>
```
This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on.
## TemplateProjectionStrategy
`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**.
### constructor
```js
constructor(
template: T,
private containerStrategy: ContainerStrategy,
private contextStrategy?: ContextStrategy,
)
```
- `template` is `TemplateRef` you would like to project.
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component.
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_)
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage.
### injectContent
```js
injectContent(): EmbeddedViewRef<T>
```
This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on.
## Predefined Projection Strategies
Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant.
### AppendComponentToBody
```js
PROJECTION_STRATEGY.AppendComponentToBody(
component: T,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Sets given context to the component and places it at the **end** of `<body>` tag in the document.
### AppendComponentToContainer
```js
PROJECTION_STRATEGY.AppendComponentToContainer(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Sets given context to the component and places it at the **end** of the container.
### AppendTemplateToContainer
```js
PROJECTION_STRATEGY.AppendTemplateToContainer(
templateRef: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Sets given context to the template and places it at the **end** of the container.
### PrependComponentToContainer
```js
PROJECTION_STRATEGY.PrependComponentToContainer(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Sets given context to the component and places it at the **beginning** of the container.
### PrependTemplateToContainer
```js
PROJECTION_STRATEGY.PrependTemplateToContainer(
templateRef: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Sets given context to the template and places it at the **beginning** of the container.
### ProjectComponentToContainer
```js
PROJECTION_STRATEGY.ProjectComponentToContainer(
component: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Clears the container, sets given context to the component, and places it **in the cleared** the container.
### ProjectTemplateToContainer
```js
PROJECTION_STRATEGY.ProjectTemplateToContainer(
templateRef: T,
containerRef: ViewContainerRef,
contextStrategy?: ComponentContextStrategy<T>,
)
```
Clears the container, sets given context to the template, and places it **in the cleared** the container.
## See Also
- [DomInsertionService](./Dom-Insertion-Service.md)

67
docs/en/UI/Angular/Service-Proxies.md

@ -0,0 +1,67 @@
## Service Proxies
It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on the server side) and **model objects** (matches to [DTOs](../../Data-Transfer-Objects) in the server side).
In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced:
* It generates a **big, single** .ts file which has some problems;
* It get **too large** when your application grows.
* It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework.
* It creates a bit **ugly code**. We want to have a clean code (just like if we write manually).
* It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies.
ABP CLI `generate-proxies` command automatically generates the typescript client proxies by creating folders which separated by module names in the `src/app` folder.
Run the following command in the **root folder** of the angular application:
```bash
abp generate-proxy
```
It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](../../CLI).
The files generated with the `--module all` option like below:
![generated-files-via-generate-proxy](./images/generated-files-via-generate-proxy.png)
### Services
Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests#restservice).
A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config)
The `providedIn` property of the services is defined as `'root'`. Therefore no need to add a service as a provider to a module. You can use a service by injecting it into a constructor as shown below:
```js
import { AbpApplicationConfigurationService } from '../app/shared/services';
//...
export class HomeComponent{
constructor(private appConfigService: AbpApplicationConfigurationService) {}
ngOnInit() {
this.appConfigService.get().subscribe()
}
}
```
The Angular compiler removes the services that have not been injected anywhere from the final output. See the [tree-shakable providers documentation](https://angular.io/guide/dependency-injection-providers#tree-shakable-providers).
### Models
The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/shared/models` folder.
There are a few [base classes](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts) in the `@abp/ng.core` package. Some models extend these classes.
A class instance can be created as shown below:
```js
import { IdentityRoleCreateDto } from '../identity/shared/models';
//...
const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true})
```
Initial values ​​can optionally be passed to each class constructor.
## What's Next?
* [HTTP Requests](./Http-Requests)

158
docs/en/UI/Angular/Toaster-Service.md

@ -0,0 +1,158 @@
# Toast Overlay
You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project.
## Getting Started
You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ToasterService } from '@abp/ng.theme.shared';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private toaster: ToasterService) {}
}
```
## Usage
You can use the `success`, `warn`, `error`, and `info` methods of `ToasterService` to display an overlay.
### How to Display a Toast Overlay
```js
this.toast.success('Message', 'Title');
```
- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`.
- `success`, `warn`, `error`, and `info` methods return the id of opened toast overlay. The toast can be removed with this id.
### How to Display a Toast Overlay With Given Options
Options can be passed as the third parameter to `success`, `warn`, `error`, and `info` methods:
```js
import { Toaster, ToasterService } from '@abp/ng.theme.shared';
//...
constructor(private toaster: ToasterService) {}
//...
const options: Partial<Toaster.ToastOptions> = {
life: 10000,
sticky: false,
closable: true,
tapToDismiss: true,
messageLocalizationParams: ['Demo', '1'],
titleLocalizationParams: []
};
this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options);
```
- `life` option is the closing time in milliseconds. Default value is `5000`.
- `sticky` option keeps toast overlay on the screen by ignoring the `life` option when `true`. Default value is `false`.
- `closable` option displays the close icon on the toast overlay when it is `true`. Default value is `true`.
- `tapToDismiss` option, when `true`, allows closing the toast overlay by clicking over it. Default value is `false`.
- `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`.
- `messageLocalizationParams` is the interpolation parameters for the localization of the message.
- `titleLocalizationParams` is the interpolation parameters for the localization of the title.
With the options above, the toast overlay looks like this:
![toast](./images/toast.png)
### How to Remove a Toast Overlay
The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast:
```js
const toastId = this.toast.success('Message', 'Title')
this.toast.remove(toastId);
```
### How to Remove All Toasts
The all open toasts can be removed manually via the `clear` method:
```js
this.toast.clear();
```
## API
### success
```js
success(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
): number
```
- `Config` namespace can be imported from `@abp/ng.core`.
- `Toaster` namespace can be imported from `@abp/ng.theme.shared`.
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts)
### warn
```js
warn(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
): number
```
### error
```js
error(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
): number
```
### info
```js
info(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
): number
```
### remove
```js
remove(id: number): void
```
Removes an open toast by the given id.
### clear
```js
clear(): void
```
Removes all open toasts.
## See Also
- [Confirmation Popup](./Confirmation-Service.md)
## What's Next?
- [Config State](./Config-State.md)

113
docs/en/UI/Angular/Track-By-Service.md

@ -0,0 +1,113 @@
# Easy TrackByFunction Implementation
`TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose.
## Getting Started
You do not have to provide the `TrackByService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. For better type support, you may pass in the type of the iterated item to it.
```js
import { TrackByService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
list: Item[];
constructor(public readonly track: TrackByService<Item>) {}
}
```
> Noticed `track` is `public` and `readonly`? That is because we will see some examples where methods of `TrackByService` instance are directly called in the component's template. That may be considered as an anti-pattern, but it has its own advantage, especially when component inheritance is leveraged. You can always use public component properties instead.
**The members are also exported as separate functions.** If you do not want to inject `TrackByService`, you can always import and use those functions directly in your classes.
## Usage
There are two approaches available.
1. You may inject `TrackByService` to your component and use its members.
2. You may use exported higher-order functions directly on component properties.
### How to Track Items by a Key
You can use `by` to get a `TrackByFunction` that tracks the iterated object based on one of its keys. For type support, you may pass in the type of the iterated item to it.
```html
<!-- template of DemoComponent -->
<div *ngFor="let item of list; trackBy: track.by('id')">{%{{{ item.name }}}%}</div>
```
`by` is exported as a stand-alone function and is named `trackBy`.
```js
import { trackBy } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackById"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackById = trackBy<Item>('id');
}
```
### How to Track by a Deeply Nested Key
You can use `byDeep` to get a `TrackByFunction` that tracks the iterated object based on a deeply nested key. For type support, you may pass in the type of the iterated item to it.
```html
<!-- template of DemoComponent -->
<div
*ngFor="let item of list; trackBy: track.byDeep('tenant', 'account', 'id')"
>
{%{{{ item.tenant.name }}}%}
</div>
```
`byDeep` is exported as a stand-alone function and is named `trackByDeep`.
```js
import { trackByDeep } from "@abp/ng.core";
@Component({
template: `
<div
*ngFor="let item of list; trackBy: trackByTenantAccountId"
>
{%{{{ item.name }}}%}
</div>
`,
})
class DemoComponent {
list: Item[];
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id');
}
```

BIN
docs/en/UI/Angular/images/confirmation.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/UI/Angular/images/generated-files-via-generate-proxy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
docs/en/UI/Angular/images/navigation-menu-after-patching.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/UI/Angular/images/navigation-menu-search-input.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/UI/Angular/images/navigation-menu-via-app-routing.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/en/UI/Angular/images/navigation-menu-via-config-state.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en/UI/Angular/images/toast.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

4
docs/en/UI/AspNetCore/Bundling-Minification.md

@ -246,10 +246,10 @@ public class MyPrismjsStyleExtension : BundleContributor
} }
```` ````
Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. Then you can configure `AbpBundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`.
````csharp ````csharp
Configure<BundleContributorOptions>(options => Configure<AbpBundleContributorOptions>(options =>
{ {
options options
.Extensions<PrismjsStyleBundleContributor>() .Extensions<PrismjsStyleBundleContributor>()

278
docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md

@ -1,3 +1,277 @@
## Dynamic Forms # Dynamic Forms
`Warning:` Before getting into this document, be sure that you have clearly understood [abp form elements](Form-elements.md) document.
## Introduction
`abp-dynamic-form` creates a bootstrap form for a given c# model.
Basic usage:
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{
new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"}
};
public void OnGet()
{
MyDetailedModel = new DetailedModel
{
Name = "",
Description = "Lorem ipsum dolor sit amet.",
IsActive = true,
Age = 65,
Day = DateTime.Now,
MyCarType = CarType.Coupe,
YourCarType = CarType.Sedan,
Country = "RU",
NeighborCountries = new List<string>() { "UK", "CA" }
};
}
public class DetailedModel
{
[Required]
[Placeholder("Enter your name...")]
[Display(Name = "Name")]
public string Name { get; set; }
[TextArea(Rows = 4)]
[Display(Name = "Description")]
[InputInfoText("Describe Yourself")]
public string Description { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Is Active")]
public bool IsActive { get; set; }
[Required]
[Display(Name = "Age")]
public int Age { get; set; }
[Required]
[Display(Name = "My Car Type")]
public CarType MyCarType { get; set; }
[Required]
[AbpRadioButton(Inline = true)]
[Display(Name = "Your Car Type")]
public CarType YourCarType { get; set; }
[DataType(DataType.Date)]
[Display(Name = "Day")]
public DateTime Day { get; set; }
[SelectItems(nameof(CountryList))]
[Display(Name = "Country")]
public string Country { get; set; }
[SelectItems(nameof(CountryList))]
[Display(Name = "Neighbor Countries")]
public List<string> NeighborCountries { get; set; }
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
## Demo
See the [dynamic forms demo page](https://bootstrap-taghelpers.abp.io/Components/DynamicForms) to see it in action.
## Attributes
### abp-model
Sets the c# model for dynamic form. Properties of this modal are turned into inputs in the form.
### submit-button
Can be `True` or `False`.
If `True`, a submit button will be generated at the bottom of the form.
Default value is `False`.
### required-symbols
Can be `True` or `False`.
If `True`, required inputs will have a symbol (*) that indicates they are required.
Default value is `True`.
## Form Content Placement
By default, `abp-dynamicform` clears the inner html and places the inputs into itself. If you want to add additional content to dynamic form or place the inputs to some specific area, you can use ` <abp-form-content />` tag. This tag will be replaced by form content and rest of the inner html of `abp-dynamic-form` tag will be unchanged.
Usage:
````xml
<abp-dynamic-form abp-model="@Model.MyExampleModel">
<div>
Some content....
</div>
<div class="input-area">
<abp-form-content />
</div>
<div>
Some more content....
</div>
</abp-dynamic-form>
````
## Input Order
`abp-dynamic-form` orders the properties by their `DisplayOrder` attribute and then their property order in model class.
Default `DisplayOrder` attribute number is 10000 for every property.
See example below:
````csharp
public class OrderExampleModel
{
[DisplayOrder(10004)]
public string Name{ get; set; }
[DisplayOrder(10005)]
public string Surname{ get; set; }
//Default 10000
public string EmailAddress { get; set; }
[DisplayOrder(10003)]
public string PhoneNumber { get; set; }
[DisplayOrder(9999)]
public string City { get; set; }
}
````
In this example, input fields will be displayed with this order: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`.
## Ignoring a property
By default, `abp-dynamic-form` generates input for every property in model class. If you want to ignore a property, use `DynamicFormIgnore` attribute.
See example below:
````csharp
public class MyModel
{
public string Name { get; set; }
[DynamicFormIgnore]
public string Surname { get; set; }
}
````
In this example, no input will be generated for `Surname` property.
## Indicating Text box, Radio Group and Combobox
If you have read the [Form elements document](Form-elements.md), you noticed that `abp-radio` and `abp-select` tags are very similar on c# model. So we have to use `[AbpRadioButton()]` attribute to tell `abp-dynamic-form` which of your properties will be radio group and which will be combobox. See example below:
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
public List<SelectListItem> CountryList { get; set; } = new List<SelectListItem>
{
new SelectListItem { Value = "CA", Text = "Canada"},
new SelectListItem { Value = "US", Text = "USA"},
new SelectListItem { Value = "UK", Text = "United Kingdom"},
new SelectListItem { Value = "RU", Text = "Russia"}
};
public void OnGet()
{
MyDetailedModel = new DetailedModel
{
ComboCarType = CarType.Coupe,
RadioCarType = CarType.Sedan,
ComboCountry = "RU",
RadioCountry = "UK"
};
}
public class DetailedModel
{
public CarType ComboCarType { get; set; }
[AbpRadioButton(Inline = true)]
public CarType RadioCarType { get; set; }
[SelectItems(nameof(CountryList))]
public string ComboCountry { get; set; }
[AbpRadioButton()]
[SelectItems(nameof(CountryList))]
public string RadioCountry { get; set; }
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
As you see in example above:
* If `[AbpRadioButton()]` are used on a **Enum** property, it will be a radio group. Otherwise, combobox.
* If `[SelectItems()]` and `[AbpRadioButton()]` are used on a property, it will be a radio group.
* If just `[SelectItems()]` is used on a property, it will be a combobox.
* If none of these attributes are used on a property, it will be a text box.
## Localization
`abp-dynamic-form` handles localization as well.
By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys and set the localization value as input label.
You can set it yourself by using `[Display()]` attribute of Asp.Net Core. You can use a localization key in this attribute. See example below:
````csharp
[Display(Name = "Name")]
public string Name { get; set; }
````
This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now.

261
docs/en/UI/AspNetCore/Tag-Helpers/Form-elements.md

@ -0,0 +1,261 @@
# Form Elements
## Introduction
Abp provides form input tag helpers to make building forms easier.
## Demo
See the [form elements demo page](https://bootstrap-taghelpers.abp.io/Components/FormElements) to see it in action.
## abp-input
`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`.
Usage:
````xml
<abp-input asp-for="@Model.MyModel.Name"/>
<abp-input asp-for="@Model.MyModel.Description"/>
<abp-input asp-for="@Model.MyModel.Password"/>
<abp-input asp-for="@Model.MyModel.IsActive"/>
````
Model:
````csharp
public class FormElementsModel : PageModel
{
public SampleModel MyModel { get; set; }
public void OnGet()
{
MyModel = new SampleModel();
}
public class SampleModel
{
[Required]
[Placeholder("Enter your name...")]
[InputInfoText("What is your name?")]
public string Name { get; set; }
[Required]
[FormControlSize(AbpFormControlSize.Large)]
public string SurName { get; set; }
[TextArea(Rows = 4)]
public string Description { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
public bool IsActive { get; set; }
}
}
````
### Attributes
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes.
#### Property Attributes
- `[TextArea()]`: Converts the input into a text area.
* `[Placeholder()]`: Sets placeholder for input. You can use a localization key directly.
* `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly.
* `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are
- `AbpFormControlSize.Default`
- `AbpFormControlSize.Small`
- `AbpFormControlSize.Medium`
- `AbpFormControlSize.Large`
* `[DisabledInput]` : Input is disabled.
* `[ReadOnlyInput]`: Input is read-only.
#### Tag Attributes
* `info`: Sets a small info text for input. You can use a localization key directly.
* `auto-focus`: If true, browser auto focuses on the element.
* `size`: Sets size of form-control wrapper element. Available values are
- `AbpFormControlSize.Default`
- `AbpFormControlSize.Small`
- `AbpFormControlSize.Medium`
- `AbpFormControlSize.Large`
* `disabled`: Input is disabled.
* `readonly`: Input is read-only.
* `label`: Sets the label for input.
* `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`.
### Label & Localization
You can set label of your input in different ways:
- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value"`.
- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core.
- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys, if `label` or `[DisplayName]` attributes are not set.
## abp-select
`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [Asp.Net Core Select Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of Asp.Net Core is also valid for `abp-select`.
`abp-select` tag needs a list of `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem ` to work. It can be provided by `asp-items` attriube on the tag or `[SelectItems()]` attribute on c# property. (if you are using [abp-dynamic-form](Dynamic-forms.md), c# attribute is the only way.)
`abp-select` supports multiple selection.
`abp-select` auto-creates a select list for **Enum** properties. No extra data is needed. If property is nullable, an empty key and value is added to top of the auto-generated list.
Usage:
````xml
<abp-select asp-for="@Model.MyModel.City" asp-items="@Model.CityList"/>
<abp-select asp-for="@Model.MyModel.AnotherCity"/>
<abp-select asp-for="@Model.MyModel.MultipleCities" asp-items="@Model.CityList"/>
<abp-select asp-for="@Model.MyModel.MyCarType"/>
<abp-select asp-for="@Model.MyModel.MyNullableCarType"/>
````
Model:
````csharp
public class FormElementsModel : PageModel
{
public SampleModel MyModel { get; set; }
public List<SelectListItem> CityList { get; set; }
public void OnGet()
{
MyModel = new SampleModel();
CityList = new List<SelectListItem>
{
new SelectListItem { Value = "NY", Text = "New York"},
new SelectListItem { Value = "LDN", Text = "London"},
new SelectListItem { Value = "IST", Text = "Istanbul"},
new SelectListItem { Value = "MOS", Text = "Moscow"}
};
}
public class SampleModel
{
public string City { get; set; }
[SelectItems(nameof(CityList))]
public string AnotherCity { get; set; }
public List<string> MultipleCities { get; set; }
public CarType MyCarType { get; set; }
public CarType? MyNullableCarType { get; set; }
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
### Attributes
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes.
#### Property Attributes
* `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above)
- `[InputInfoText()]`: Sets a small info text for input. You can use a localization key directly.
- `[FormControlSize()]`: Sets size of form-control wrapper element. Available values are
- `AbpFormControlSize.Default`
- `AbpFormControlSize.Small`
- `AbpFormControlSize.Medium`
- `AbpFormControlSize.Large`
#### Tag Attributes
- `asp-items`: Sets the select data. This Should be a list of SelectListItem.
- `info`: Sets a small info text for input. You can use a localization key directly.
- `size`: Sets size of form-control wrapper element. Available values are
- `AbpFormControlSize.Default`
- `AbpFormControlSize.Small`
- `AbpFormControlSize.Medium`
- `AbpFormControlSize.Large`
- `label`: Sets the label for input.
- `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`.
### Label & Localization
You can set label of your input in different ways:
- You can use `Label` attribute and directly set the label. But it doesn't auto localize your localization key. So use it as `label="@L["{LocalizationKey}"].Value".`
- You can set it using `[Display(name="{LocalizationKey}")]` attribute of Asp.Net Core.
- You can just let **abp** find the localization key for the property. It will try to find "DisplayName:{PropertyName}" or "{PropertyName}" localization keys.
Localizations of combobox values are set by `abp-select` for **Enum** property. It searches for "{EnumTypeName}.{EnumPropertyName}" or "{EnumPropertyName}" localization keys. For instance, in the example above, it will use "CarType.StationWagon" or "StationWagon" keys for localization when it localizes combobox values.
## abp-radio
`abp-radio` tag creates a Bootstrap form radio group for a given c# property. Usage is very similar to `abp-select` tag.
Usage:
````xml
<abp-radio asp-for="@Model.MyModel.CityRadio" asp-items="@Model.CityList" inline="true"/>
<abp-radio asp-for="@Model.MyModel.CityRadio2"/>
````
Model:
````csharp
public class FormElementsModel : PageModel
{
public SampleModel MyModel { get; set; }
public List<SelectListItem> CityList { get; set; } = new List<SelectListItem>
{
new SelectListItem { Value = "NY", Text = "New York"},
new SelectListItem { Value = "LDN", Text = "London"},
new SelectListItem { Value = "IST", Text = "Istanbul"},
new SelectListItem { Value = "MOS", Text = "Moscow"}
};
public void OnGet()
{
MyModel = new SampleModel();
MyModel.CityRadio = "IST";
MyModel.CityRadio2 = "MOS";
}
public class SampleModel
{
public string CityRadio { get; set; }
[SelectItems(nameof(CityList))]
public string CityRadio2 { get; set; }
}
}
````
### Attributes
You can set some of the attributes on your c# property, or directly on html tag. If you are going to use this property in a [abp-dynamic-form](Dynamic-forms.md), then you can only set these properties via property attributes.
#### Property Attributes
- `[SelectItems()]`: Sets the select data. Parameter should be the name of the data list. (see example above)
#### Tag Attributes
- `asp-items`: Sets the select data. This Should be a list of SelectListItem.
- `Inline`: If true, radio buttons will be in single line, next to each other. If false, they will be under each other.

14
docs/en/UI/AspNetCore/Tag-Helpers/Index.md

@ -12,18 +12,18 @@ ABP Framework also adds some **useful features** to the standard bootstrap compo
Here, the list of components those are wrapped by the ABP Framework: Here, the list of components those are wrapped by the ABP Framework:
* [Alerts](Alerts.md)
* [Buttons](Buttons.md) * [Buttons](Buttons.md)
* [Cards](Cards.md) * [Cards](Cards.md)
* [Alerts](Alerts.md)
* [Tabs](Tabs.md)
* [Grids](Grids.md)
* [Modals](Modals.md)
* [Collapse](Collapse.md) * [Collapse](Collapse.md)
* [Dropdowns](Dropdowns.md) * [Dropdowns](Dropdowns.md)
* [Grids](Grids.md)
* [List Groups](List-Groups.md) * [List Groups](List-Groups.md)
* [Modals](Modals.md)
* [Paginator](Paginator.md) * [Paginator](Paginator.md)
* [Popovers](Popovers.md) * [Popovers](Popovers.md)
* [Progress Bars](Progress-Bars.md) * [Progress Bars](Progress-Bars.md)
* [Tabs](Tabs.md)
* [Tooltips](Tooltips.md) * [Tooltips](Tooltips.md)
* ... * ...
@ -31,8 +31,8 @@ Here, the list of components those are wrapped by the ABP Framework:
## Form Elements ## Form Elements
See [demo](https://bootstrap-taghelpers.abp.io/Components/FormElements). **Abp Tag Helpers** add new features to standard **Asp.Net Core MVC input & select Tag Helpers** and wrap them with **Bootstrap** form controls. See [Form Elements documentation](Form-elements.md) .
## Dynamic Inputs ## Dynamic Forms
See [demo](https://bootstrap-taghelpers.abp.io/Components/DynamicForms). **Abp Tag helpers** offer an easy way to build complete **Bootstrap forms**. See [Dynamic Forms documentation](Dynamic-Forms.md).

2
docs/en/UI/AspNetCore/Tag-Helpers/List-Groups.md

@ -19,7 +19,7 @@ Basic usage:
## Demo ## Demo
See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroups) to see it in action. See the [list groups demo page](https://bootstrap-taghelpers.abp.io/Components/ListGroup) to see it in action.
## Attributes ## Attributes

42
docs/en/UI/AspNetCore/Tag-Helpers/Popovers.md

@ -0,0 +1,42 @@
# Popovers
## Introduction
`abp-popover` is the abp tag for popover messages.
Basic usage:
````xml
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
````
## Demo
See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action.
## Attributes
### disabled
A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values:
* `false` (default value)
* `true`
### dismissable
A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values:
* `false` (default value)
* `true`
### hoverable
A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values:
* `false` (default value)
* `true`

4
docs/en/UI/AspNetCore/Tag-Helpers/Progress-Bars.md

@ -26,7 +26,7 @@ Basic usage:
## Demo ## Demo
See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progress-Bars) to see it in action. See the [progress bars demo page](https://bootstrap-taghelpers.abp.io/Components/Progressbars) to see it in action.
## Attributes ## Attributes
@ -67,4 +67,4 @@ A value indicates if the background style of the progress bar is stripped. Shoul
A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values: A value indicates if the stripped background style of the progress bar is animated. Should be one of the following values:
* `false` (default value) * `false` (default value)
* `true` * `true`

2
docs/en/UI/AspNetCore/Tag-Helpers/Tabs.md

@ -30,7 +30,7 @@ Basic usage:
## Demo ## Demo
See the [cards demo page](https://bootstrap-taghelpers.abp.io/Components/Cards) to see it in action. See the [tabs demo page](https://bootstrap-taghelpers.abp.io/Components/Tabs) to see it in action.
## abp-tab Attributes ## abp-tab Attributes

2
docs/en/UI/AspNetCore/Widgets.md

@ -502,4 +502,4 @@ Configure<AbpWidgetOptions>(options =>
## See Also ## See Also
* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). * [Example project (source code)](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo).

1616
docs/en/UI/Common/Utils/Linked-List.md

File diff suppressed because it is too large

96
docs/en/docs-nav.json

@ -5,16 +5,7 @@
"items": [ "items": [
{ {
"text": "From Startup Templates", "text": "From Startup Templates",
"items": [ "path": "Getting-Started.md"
{
"text": "Application with MVC (Razor Pages) UI",
"path": "Getting-Started-AspNetCore-MVC-Template.md"
},
{
"text": "Application with Angular UI",
"path": "Getting-Started-Angular-Template.md"
}
]
}, },
{ {
"text": "From Empty Projects", "text": "From Empty Projects",
@ -74,6 +65,10 @@
} }
] ]
}, },
{
"text": "\"How to\" Guides",
"path": "How-To/Index.md"
},
{ {
"text": "Migrating from the ASP.NET Boilerplate", "text": "Migrating from the ASP.NET Boilerplate",
"path": "AspNet-Boilerplate-Migration-Guide.md" "path": "AspNet-Boilerplate-Migration-Guide.md"
@ -150,6 +145,10 @@
{ {
"text": "Data Filtering", "text": "Data Filtering",
"path": "Data-Filtering.md" "path": "Data-Filtering.md"
},
{
"text": "Object Extensions",
"path": "Object-Extensions.md"
} }
] ]
}, },
@ -222,8 +221,11 @@
}, },
{ {
"text": "Domain Driven Design", "text": "Domain Driven Design",
"path": "Domain-Driven-Design.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Domain-Driven-Design.md"
},
{ {
"text": "Domain Layer", "text": "Domain Layer",
"items": [ "items": [
@ -293,7 +295,17 @@
}, },
{ {
"text": "Tag Helpers", "text": "Tag Helpers",
"path": "UI/AspNetCore/Tag-Helpers/Index.md" "path": "UI/AspNetCore/Tag-Helpers/Index.md",
"items": [
{
"text": "Form Elements",
"path": "UI/AspNetCore/Tag-Helpers/Form-elements.md"
},
{
"text": "Dynamic Forms",
"path": "UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md"
}
]
}, },
{ {
"text": "Widgets", "text": "Widgets",
@ -312,6 +324,14 @@
{ {
"text": "Angular", "text": "Angular",
"items": [ "items": [
{
"text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md"
},
{
"text": "HTTP Requests",
"path": "UI/Angular/HTTP-Requests.md"
},
{ {
"text": "Localization", "text": "Localization",
"path": "UI/Angular/Localization.md" "path": "UI/Angular/Localization.md"
@ -320,10 +340,22 @@
"text": "Permission Management", "text": "Permission Management",
"path": "UI/Angular/Permission-Management.md" "path": "UI/Angular/Permission-Management.md"
}, },
{
"text": "Confirmation Popup",
"path": "UI/Angular/Confirmation-Service.md"
},
{
"text": "Toast Overlay",
"path": "UI/Angular/Toaster-Service.md"
},
{ {
"text": "Config State", "text": "Config State",
"path": "UI/Angular/Config-State.md" "path": "UI/Angular/Config-State.md"
}, },
{
"text": "Modifying the Menu",
"path": "UI/Angular/Modifying-the-Menu.md"
},
{ {
"text": "Component Replacement", "text": "Component Replacement",
"path": "UI/Angular/Component-Replacement.md" "path": "UI/Angular/Component-Replacement.md"
@ -331,6 +363,36 @@
{ {
"text": "Custom Setting Page", "text": "Custom Setting Page",
"path": "UI/Angular/Custom-Setting-Page.md" "path": "UI/Angular/Custom-Setting-Page.md"
},
{
"text": "Lazy Loading Scripts & Styles",
"path": "UI/Angular/Lazy-Load-Service.md"
},
{
"text": "DomInsertionService",
"path": "UI/Angular/Dom-Insertion-Service.md"
},
{
"text": "ContentProjectionService",
"path": "UI/Angular/Content-Projection-Service.md"
},
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"
}
]
},
{
"text": "Common",
"items": [
{
"text": "Utilities",
"items": [
{
"text": "Linked List (Doubly)",
"path": "UI/Common/Utils/Linked-List.md"
}
]
} }
] ]
} }
@ -338,8 +400,11 @@
}, },
{ {
"text": "Data Access", "text": "Data Access",
"path": "Data-Access.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Data-Access.md"
},
{ {
"text": "Connection Strings", "text": "Connection Strings",
"path": "Connection-Strings.md" "path": "Connection-Strings.md"
@ -422,8 +487,11 @@
}, },
{ {
"text": "Startup Templates", "text": "Startup Templates",
"path": "Startup-Templates/Index.md",
"items": [ "items": [
{
"text": "Overall",
"path": "Startup-Templates/Index.md"
},
{ {
"text": "Application", "text": "Application",
"path": "Startup-Templates/Application.md" "path": "Startup-Templates/Application.md"

BIN
docs/en/images/angular-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/en/images/angular-template-structure-diagram.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png → docs/en/images/bookstore-home.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/images/bookstore-login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save