Browse Source

merge master

pull/4688/head
nuck.tang 6 years ago
parent
commit
0edeaca62c
  1. 35
      README.md
  2. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json
  3. 9
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json
  4. 14
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  5. 156
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json
  6. 23
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json
  7. 15
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json
  8. 156
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json
  9. 2
      common.props
  10. 6
      configureawait.props
  11. 2
      docs/en/Blob-Storing.md
  12. 1
      docs/en/CLI.md
  13. 133
      docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md
  14. 298
      docs/en/Distributed-Event-Bus.md
  15. 63
      docs/en/Entity-Framework-Core-Oracle-Devart.md
  16. 58
      docs/en/Entity-Framework-Core-Oracle-Official.md
  17. 60
      docs/en/Entity-Framework-Core-Oracle.md
  18. 9
      docs/en/Event-Bus.md
  19. 72
      docs/en/Getting-Started-React-Native.md
  20. 146
      docs/en/Getting-Started.md
  21. 6
      docs/en/Index.md
  22. 226
      docs/en/Local-Event-Bus.md
  23. 2
      docs/en/Modules/Identity.md
  24. 2
      docs/en/Modules/Index.md
  25. 47
      docs/en/Modules/Organization-Units.md
  26. 3
      docs/en/RabbitMq.md
  27. 128
      docs/en/Repositories.md
  28. 1
      docs/en/Road-Map.md
  29. 4
      docs/en/Samples/Index.md
  30. 2
      docs/en/Samples/Microservice-Demo.md
  31. 3
      docs/en/SignalR-Integration.md
  32. 985
      docs/en/Tutorials/Part-1.md
  33. 1542
      docs/en/Tutorials/Part-2.md
  34. 1227
      docs/en/Tutorials/Part-3.md
  35. 248
      docs/en/Tutorials/Part-4.md
  36. 401
      docs/en/Tutorials/Part-5.md
  37. BIN
      docs/en/Tutorials/images/bookstore-actions-buttons.png
  38. BIN
      docs/en/Tutorials/images/bookstore-angular-file-tree.png
  39. BIN
      docs/en/Tutorials/images/bookstore-book-and-booktype.png
  40. BIN
      docs/en/Tutorials/images/bookstore-book-list-3.png
  41. BIN
      docs/en/Tutorials/images/bookstore-book-list.png
  42. BIN
      docs/en/Tutorials/images/bookstore-confirmation-popup.png
  43. BIN
      docs/en/Tutorials/images/bookstore-dbmigrator-on-solution.png
  44. BIN
      docs/en/Tutorials/images/bookstore-edit-button-2.png
  45. BIN
      docs/en/Tutorials/images/bookstore-edit-delete-actions.png
  46. BIN
      docs/en/Tutorials/images/bookstore-empty-new-book-modal.png
  47. BIN
      docs/en/Tutorials/images/bookstore-getlist-result-network.png
  48. BIN
      docs/en/Tutorials/images/bookstore-index-js-file-v3.png
  49. BIN
      docs/en/Tutorials/images/bookstore-javascript-proxy-console.png
  50. BIN
      docs/en/Tutorials/images/bookstore-new-book-button-2.png
  51. BIN
      docs/en/Tutorials/images/bookstore-new-book-button-small.png
  52. BIN
      docs/en/Tutorials/images/bookstore-permissions-ui.png
  53. BIN
      docs/en/Tutorials/images/generated-proxies-2.png
  54. 71
      docs/en/UI/Angular/Config-State.md
  55. 51
      docs/en/UI/Angular/Custom-Setting-Page.md
  56. 114
      docs/en/UI/Angular/List-Service.md
  57. 21
      docs/en/UI/Angular/Localization.md
  58. 477
      docs/en/UI/Angular/Migration-Guide-v3.md
  59. 289
      docs/en/UI/Angular/Modifying-the-Menu.md
  60. 16
      docs/en/UI/Angular/Permission-Management.md
  61. 6
      docs/en/UI/Angular/Service-Proxies.md
  62. BIN
      docs/en/UI/Angular/images/custom-settings.png
  63. BIN
      docs/en/UI/Angular/images/generated-files-via-generate-proxy.png
  64. BIN
      docs/en/UI/Angular/images/navigation-menu-search-input.png
  65. BIN
      docs/en/UI/Angular/images/table-of-packages-to-update.png
  66. 290
      docs/en/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md
  67. 357
      docs/en/Unit-Of-Work.md
  68. 180
      docs/en/Virtual-File-System.md
  69. 45
      docs/en/docs-nav.json
  70. 8
      docs/pt-BR/Tutorials/Angular/Part-II.md
  71. 2
      docs/zh-Hans/Blob-Storing.md
  72. 3
      docs/zh-Hans/CLI.md
  73. 134
      docs/zh-Hans/Distributed-Event-Bus-RabbitMQ-Integration.md
  74. 300
      docs/zh-Hans/Distributed-Event-Bus.md
  75. 4
      docs/zh-Hans/Entity-Framework-Core-MySQL.md
  76. 60
      docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md
  77. 67
      docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md
  78. 67
      docs/zh-Hans/Entity-Framework-Core-Oracle.md
  79. 4
      docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md
  80. 4
      docs/zh-Hans/Entity-Framework-Core-SQLite.md
  81. 11
      docs/zh-Hans/Event-Bus.md
  82. 227
      docs/zh-Hans/Local-Event-Bus.md
  83. 131
      docs/zh-Hans/Repositories.md
  84. 3
      docs/zh-Hans/SignalR-Integration.md
  85. 224
      docs/zh-Hans/Tutorials/Part-1.md
  86. 606
      docs/zh-Hans/Tutorials/Part-2.md
  87. BIN
      docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png
  88. BIN
      docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png
  89. BIN
      docs/zh-Hans/Tutorials/images/bookstore-book-list.png
  90. BIN
      docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png
  91. BIN
      docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png
  92. 89
      docs/zh-Hans/UI/Angular/List-Service.md
  93. 363
      docs/zh-Hans/UI/Angular/Migration-Guide-v3.md
  94. 220
      docs/zh-Hans/UI/Angular/Modifying-the-Menu.md
  95. 8
      docs/zh-Hans/UI/Angular/Permission-Management.md
  96. BIN
      docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png
  97. 278
      docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md
  98. 356
      docs/zh-Hans/Unit-Of-Work.md
  99. 172
      docs/zh-Hans/Virtual-File-System.md
  100. 22
      docs/zh-Hans/docs-nav.json

35
README.md

@ -1,34 +1,23 @@
# ABP
# ABP Framework
![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg)
[![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
[![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds)
[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
This project is the next generation of the [ASP.NET Boilerplate](https://aspnetboilerplate.com/) web application framework. See [the announcement](https://blog.abp.io/abp/Abp-vNext-Announcement).
ABP is an **open source application framework** focused on ASP.NET Core based web application development, but also supports developing other type of applications.
See the official [web site (abp.io)](https://abp.io/) for more information.
## Links
### Documentation
* <a href="https://abp.io/" target="_blank">Official Web Site</a>
* <a href="https://abp.io/get-started" target="_blank">Get Started</a>
* <a href="https://abp.io/features" target="_blank">Features</a>
* <a href="https://docs.abp.io/" target="_blank">Documentation</a>
* <a href="https://docs.abp.io/en/abp/latest/Samples/Index" target="_blank">Samples</a>
* <a href="https://blog.abp.io/" target="_blank">Blog</a>
* <a href="https://stackoverflow.com/questions/tagged/abp" target="_blank">Stack overflow</a>
* <a href="https://twitter.com/abpframework" target="_blank">Twitter</a>
See the <a href="https://docs.abp.io/" target="_blank">documentation</a>.
### Development
#### Pre Requirements
- Visual Studio 2019 16.4.0+
#### Framework
Framework solution is located under the `framework` folder. It has no external dependency.
#### Modules/Templates
[Modules](modules/) and [Templates](templates/) have their own solutions and have **local references** to the framework and each other.
Visual Studio can not work properly with the local references out of the solution folder. When you open a module/sample solution in the Visual Studio, you may get some errors related to the dependencies. In this case, run the `dotnet restore` on the command prompt for the related solution's folder. You need to run it after you first open the solution or change a dependency.
### Contribution
## Contribution
ABP is an open source platform. Check [the contribution guide](docs/en/Contribution/Index.md) if you want to contribute to the project.

3
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json

@ -8,6 +8,7 @@
"FrameworkDocumentation": "Framework documentation",
"OfficialBlog": "Official blog",
"CommercialHomePage": "Commercial home page",
"CommercialSupportWebSite": "Commercial support web site"
"CommercialSupportWebSite": "Commercial support web site",
"CommunityWebSite": "ABP community web site"
}
}

9
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json

@ -1,5 +1,14 @@
{
"culture": "tr",
"texts": {
"Account": "ABP Hesabı - Giriş Yap & Kayıt Ol | ABP.IO",
"Welcome": "Hoş Geldiniz",
"UseOneOfTheFollowingLinksToContinue": "Devam etmek için linkleri kullanabilirsiniz",
"FrameworkHomePage": "Framework ana sayfa",
"FrameworkDocumentation": "Framework Dokümantasyon",
"OfficialBlog": "Resmi blog",
"CommercialHomePage": "Kurumsal ana sayfa",
"CommercialSupportWebSite": "Kurumsal destek web sitesi",
"CommunityWebSite": "ABP topluluk web sitesi"
}
}

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

@ -76,6 +76,7 @@
"Organizations": "Organizations",
"LongName": "Long name",
"LicenseType": "License type",
"MissingLicenseTypeField": "The license type field is required!",
"LicenseStartTime": "License start time",
"LicenseEndTime": "License end time",
"AllowedDeveloperCount": "Allowed developer count",
@ -101,7 +102,16 @@
"DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules",
"OrganizationName": "Organization name",
"CreationDate": "Creation date",
"LicenseStart": "License start date",
"LicenseEndDate": "License end date",
"OrganizationNamePlaceholder": "Organization name...",
"TotalQuestionCountPlaceholder": "Total question count...",
"RemainingQuestionCountPlaceholder": "Remaining question count...",
"LicenseTypePlaceholder": "License type...",
"CreationDatePlaceholder": "Creation date...",
"LicenseStartDatePlaceholder": "License start date...",
"LicenseEndDatePlaceholder": "License end date...",
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
@ -121,6 +131,7 @@
"TaxNumber": "Tax Number",
"InvoiceNumber": "Invoice Number",
"InvoiceDate": "Invoice Date",
"InvoiceNote": "Invoice Note",
"Quantity": "Quantity",
"AddProduct": "Add Product",
"AddProductWarning": "You need to add product!",
@ -154,6 +165,7 @@
"RemainingQuestionCount": "Remaining question count",
"TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !",
"QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !",
"UnlimitedQuestionCount": "Unlimited question count"
"UnlimitedQuestionCount": "Unlimited question count",
"Notes": "Notes"
}
}

156
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json

@ -1,5 +1,161 @@
{
"culture": "tr",
"texts": {
"Permission:Organizations": "Organizasyonlar",
"Permission:Manage": "Organizasyonları Yönet",
"Permission:DiscountRequests": "İndirim Talepleri",
"Permission:DiscountManage": "İndirim Taleplerini Yönet",
"Permission:Disable": "Devre Dışı Bırak",
"Permission:Enable": "Etkinleşir",
"Permission:EnableSendEmail": "E-Posta Göndermeyi Etkinleştir",
"Permission:SendEmail": "E-Posta Gönder",
"Permission:NpmPackages": "NPM Paketleri",
"Permission:NugetPackages": "Nuget Paketleri",
"Permission:Maintenance": "Bakım",
"Permission:Maintain": "Bakım Yap",
"Permission:ClearCaches": "Önbelleği temizle",
"Permission:Modules": "Modüller",
"Permission:Packages": "Paketler",
"Permission:Edit": "Güncelle",
"Permission:Delete": "Sil",
"Permission:Create": "Oluştur",
"Permission:Accounting": "Muhasebe",
"Permission:Accounting:Quotation": "Fiyatlandırma",
"Permission:Accounting:Invoice": "Fatura",
"Menu:Organizations": "Organizasyonlar",
"Menu:Accounting": "Muhasebe",
"Menu:Packages": "Paketler",
"Menu:DiscountRequests": "İndirim Talepleri",
"NpmPackageDeletionWarningMessage": "Bu NPM Paketi silinecektir. Onaylıyor musunuz?",
"NugetPackageDeletionWarningMessage": "Bu Nuget Paketi silinecektir. Onaylıyor musunuz?",
"ModuleDeletionWarningMessage": "Bu Modül silinecektir. Onaylıyor musunuz?",
"Name": "İsim",
"DisplayName": "Görüntülenen isim",
"ShortDescription": "Kısa açıklama",
"NameFilter": "İsim",
"CreationTime": "Oluşturma zamanı",
"IsPro": "Is pro",
"ShowOnModuleList": "Modül listesinde göster",
"EfCoreConfigureMethodName": "Metot adını yapılandır",
"IsProFilter": "Is pro",
"ApplicationType": "Uygulama tipi",
"Target": "Hedef",
"TargetFilter": "Hedef",
"ModuleClass": "Modül sınıfı",
"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": "Güncelle",
"Delete": "Sil",
"Refresh": "Yenile",
"NpmPackages": "NPM Paketleri",
"NugetPackages": "Nuget Paketleri",
"NpmPackageCount": "NPM Paket Sayısı",
"NugetPackageCount": "Nuget Paket Sayısı",
"Module": "Modüller",
"ModuleInfo": "Modül bilgisi",
"CreateANpmPackage": "Bir NPM paketi oluştur",
"CreateAModule": "Bir modül oluştur",
"CreateANugetPackage": "Bir Nuget paketi oluştur",
"AddNew": "Yenisini Ekle",
"PackageAlreadyExist{0}": "\"{0}\"isimli paket zaten eklendi.",
"ModuleAlreadyExist{0}": "\"{0}\" isimli modül zaten eklendi.",
"ClearCache": "Önbelleği Temizle",
"SuccessfullyCleared": "Başarıyla temizlendi",
"Menu:NpmPackages": "NPM Paketleri",
"Menu:Modules": "Modüller",
"Menu:Maintenance": "Bakım",
"Menu:NugetPackages": "Nuget Paketleri",
"CreateAnOrganization": "Bir organizasyon oluştur",
"Organizations": "Organizasyonlar",
"LongName": "Uzun isim",
"LicenseType": "Lisans tipi",
"MissingLicenseTypeField": "Lisans tipi alanı zorunludur.",
"LicenseStartTime": "Lisans başlama zamanı",
"LicenseEndTime": "Lisans bitiş zamanı",
"AllowedDeveloperCount": "İzin verilen developer sayısı",
"UserNameOrEmailAddress": "Kullanıcı adı veya e-posta adresi",
"AddOwner": "Owner ekle",
"UserName": "Kullanıcı Adı",
"Email": "E-Posta",
"Developers": "Developers",
"AddDeveloper": "Developer Ekle",
"Create": "Oluştur",
"UserNotFound": "Kullanıcı bulunamadı",
"{0}WillBeRemovedFromDevelopers": "{0} kullanıcı adlı developer silinecektir, Onaylıyor musunuz?",
"{0}WillBeRemovedFromOwners": "{0} kullanıcı adlı owner silinecektir, Onaylıyor musunuz?",
"Computers": "Bilgisayarlar",
"UniqueComputerId": "Özgün bilgisayar id",
"LastSeenDate": "Son görülme tarihi",
"{0}Computer{1}WillBeRemovedFromRecords": "{0} kullanıcı isimli kullanıcının bilgisayarı ({1}) kayıtlardan kaldırılacaktır",
"OrganizationDeletionWarningMessage": "Organizasyon silinecektir.",
"DeletingLastOwnerWarningMessage": "Bir organizasyon en az bir ownera sahip olmalıdır! Bu nedenle bu ownerı kaldıramazsınız",
"This{0}AlreadyExistInThisOrganization": "{0} zaten bu organizasyonda bulunmaktadır",
"AreYouSureYouWantToDeleteAllComputers": "Tüm bilgisayaları silmek istediğinize emin misiniz?",
"DeleteAll": "Tümünü sil",
"DoYouWantToCreateNewUser": "Yeni kullanıcı oluşturmak istiyor musunuz?",
"MasterModules": "Master Modüller",
"OrganizationName": "Organizasyon adı",
"OrganizationNamePlaceholder": "Organizasyon adı...",
"UsernameOrEmail": "Kullanıcı adı veya e-posta",
"UsernameOrEmailPlaceholder": "Kullanıcı adı veya e-posta",
"Member": "Üye",
"PurchaseOrderNo": "Satın alma sipariş no",
"QuotationDate": "Fiyatlandırma tarihi",
"CompanyName": "Şirket adı",
"CompanyAddress": "Şirket adresi",
"Price": "Fiyat",
"DiscountText": "İndirim metni",
"DiscountQuantity": "İndirim miktarı",
"DiscountPrice": "İndirim fiyatı",
"Quotation": "Fiyatlandırma",
"ExtraText": "Eksta metin",
"ExtraAmount": "Eksta miktar",
"DownloadQuotation": "Fiyatlandırmayı İndir",
"Invoice": "Fatura",
"TaxNumber": "Vergi Numarası",
"InvoiceNumber": "Fatura Numarası",
"InvoiceDate": "Fatura Tarihi",
"InvoiceNote": "Fatura Notu",
"Quantity": "Miktar",
"AddProduct": "Ürün Ekle",
"AddProductWarning": "Ürün eklemelisiniz!",
"TotalPrice": "Toplam Fiyat",
"Generate": "Üret",
"MissingQuantityField": "Miktar alanı zorunludur!",
"MissingPriceField": "Fiyat alanı zorunludur!",
"CodeUsageStatus": "Statü",
"Country": "Ülke",
"DeveloperCount": "Developer Sayısı",
"RequestCode": "Talep Kodu",
"WebSite": "Web Sitesi",
"GithubUsername": "Github Kullanıcı adı",
"PhoneNumber": "Telefon Numarası",
"ProjectDescription": "Proje Açıklaması",
"Referrer": "Yönlendiren",
"DiscountRequests": "İndirim Talebi",
"Copylink": "Kopyalama Linki",
"Disable": "Devre Dışı Bırak",
"Enable": "Etkinleştir",
"EnableSendEmail": "E-Posta Göndermeyi Etkinleştir",
"SendEmail": "E-Posta Gönder",
"SuccessfullyDisabled": "Başarıyla Devre Dışı Bırakıldı",
"SuccessfullyEnabled": "Başarıyla Etkinleştirildi",
"EmailSent": "E-Posta Gönderildi",
"SuccessfullySent": "Başarıyla Gönderildi",
"SuccessfullyDeleted": "Başarıyla Silindi",
"DiscountRequestDeletionWarningMessage": "İndirim talebi silinecektir",
"BusinessType": "İş tipi",
"TotalQuestionCount": "Toplam soru sayısı",
"RemainingQuestionCount": "Kalan soru sayısı",
"TotalQuestionMustBeGreaterWarningMessage": "Toplam soru sayısı kalan soru sayısından büyük olmalıdır!",
"QuestionCountsMustBeGreaterThanZero": "Toplam soru sayısı ve kalan soru sayısı sıfır veya sıfırdan daha büyük olmalıdır!",
"UnlimitedQuestionCount": "Sınırsız soru sayısı"
}
}

23
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json

@ -1,4 +1,4 @@
{
{
"culture": "tr",
"texts": {
"Volo.AbpIo.Domain:010004": "Maksimum üye sayısı aşıldı!",
@ -8,7 +8,24 @@
"Volo.AbpIo.Domain:010008": "Maksimum izin verilen kullanıcı sayısı mevcut kullanıcı sayısından az olamaz!",
"Volo.AbpIo.Domain:010009": "Maksimum izin verilen kullanıcı sayısı sıfırdan az olamaz!",
"Volo.AbpIo.Domain:010010": "Maksimum mac adresi sayısı geçildi!",
"Volo.AbpIo.Domain:010011": "Bireysel lisans birden fazla geliştiriciye sahip olamaz!",
"Volo.AbpIo.Domain:010012": "Lisans, lisans süresi bittikten bir ay sonra uzatılamaz!",
"Volo.AbpIo.Domain:010011": "Bireysel lisans birden fazla geliştiriciye sahip olamaz!",
"Volo.AbpIo.Domain:010012": "Lisans, lisans süresi bittikten bir ay sonra uzatılamaz!",
"Volo.AbpIo.Domain:020001": "Bu NPM Paketi silinemiyor çünkü \"{NugetPackages}\" Nuget Paketleri bu pakete bağımlı.",
"Volo.AbpIo.Domain:020002": "Bu NPM Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor.",
"Volo.AbpIo.Domain:020003": "Bu NPM Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor ve \"{NugetPackages}\" Nuget Paketleri bu pakete bağımlı.",
"Volo.AbpIo.Domain:020004": "Bu Nuget Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor.",
"WantToLearn?": "Öğrenmek ister misin?",
"ReadyToGetStarted?": "Başlamaya hazır mısın?",
"JoinOurCommunity": "Topluluğumuza katılın",
"GetStartedUpper": "BAŞLAYIN",
"ForkMeOnGitHub": "Fork me on GitHub",
"Features": "Özellikler",
"GetStarted": "Başlayın",
"Documents": "Dokümanlar",
"Community": "Topluluk",
"ContributionGuide": "Katkı Rehberi",
"Blog": "Blog",
"Commercial": "Ticari",
"SeeDocuments": "Dokümanlara Göz Atın"
}
}

15
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json

@ -1,4 +1,4 @@
{
{
"culture": "tr",
"texts": {
"OrganizationManagement": "Organizasyon yönetimi",
@ -6,6 +6,8 @@
"Volo.AbpIo.Commercial:010003": "Bu organizasyonda yetkili değilsiniz!",
"OrganizationNotFoundMessage": "Organizasyon bulunamadı!",
"DeveloperCount": "Yazılımcı sayısı",
"QuestionCount": "Kalan / toplam sorular",
"Unlimited": "Sınırsız",
"Owners": "Yetkili sayısı",
"AddMember": "Üye ekle",
"AddOwner": "Yetkili ekle",
@ -19,12 +21,15 @@
"StartDate": "Başlangıç tarihi",
"EndDate": "bitiş tarihi",
"Modules": "Modüller",
"Volo.AbpIo.Commercial:010004": "Kullanıcı bulunamadı! İlgili kullanıcının daha önceden sisteme kayıt olmuş olması gerekiyor.",
"LicenseExtendMessage": "Lisans bitiş tarihiniz {0} tarihine kadar uzatıldı",
"LicenseUpgradeMessage": "Lisansınız {0} lisansa yükseltildi",
"LicenseAddDeveloperMessage": "Lisansınıza {0} geliştirici eklendi",
"MyOrganizations": "Organizasyonlarım",
"ApiKey": "API anahtarı",
"UserNameNotFound": "{0} kullanıcı adı ile bir kullanıcı yok"
"Volo.AbpIo.Commercial:010004": "Kullanıcı bulunamadı! İlgili kullanıcının daha önceden sisteme kayıt olmuş olması gerekiyor.",
"MyOrganizations": "Organizasyonlarım",
"ApiKey": "API anahtarı",
"UserNameNotFound": "{0} kullanıcı adı ile bir kullanıcı yok",
"SuccessfullyAddedToNewsletter": "Bültenimize abone olduğunuz için teşekkürler!",
"MyProfile": "Profilim",
"EmailNotValid": "Lütfen uygun bir e-posta adresi giriniz"
}
}

156
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json

@ -1,5 +1,161 @@
{
"culture": "tr",
"texts": {
"GetStarted": "Başlamak - Başlangıç Templateleri",
"Create": "Oluştur",
"NewProject": "Yeni Proje",
"DirectDownload": "Doğrudan İndir",
"ProjectName": "Proje ismi",
"ProjectType": "Proje tipi",
"DatabaseProvider": "Veritabanı sağlayacısı",
"NTier": "N-Tier",
"IncludeUserInterface": "Kullanıcı arayüzünü dahil et",
"CreateNow": "Şimdi oluştur",
"TheStartupProject": "Başlangıç projesi",
"Tutorial": "Öğretici",
"UsingCLI": "CLI Kullanmak",
"SeeDetails": "Detayları Görüntüle",
"AbpShortDescription": "ABP modern web uygulamaları geliştirmek için eksiksiz bir mimari ve günlü bir altyapıdır! Size SOLID geliştirme tecrübelerini sunmak için en iyi uygulama ve kuralları takip eder.",
"SourceCodeUpper": "KAYNAK KOD",
"LatestReleaseLogs": "Son release kayıtları",
"Infrastructure": "Altyapı",
"Architecture": "Mimari",
"Modular": "Modüler",
"DontRepeatYourself": "Kendini Tekrar Etme",
"DeveloperFocused": "Developer Odaklı",
"FullStackApplicationInfrastructure": "Full stack uygulama altyapısı",
"DomainDrivenDesign": "Domain Driven Design",
"DomainDrivenDesignExplanation": "DDD paterni ve prensiplerinden yola çıkarak dizayn edildi ve geliştirildi. Uygulamanız için katmanlı bir model sunmaktadır.",
"Authorization": "Yetkilendirme",
"AuthorizationExplanation": "Kullanıcı, rol ve ayrıntılı izin sistemi ile modern yetkilendirme. Microsoft Identity library üzerine kurulmuştur.",
"MultiTenancy": "Multi-Tenancy",
"MultiTenancyExplanationShort": "SaaS uygulamaları kolaylaştı! Veritababından kullanıcı arayüzüne entegre edilmiş multi-tenancy",
"CrossCuttingConcerns": "Cross Cutting Concerns",
"CrossCuttingConcernsExplanationShort": "Yetkilendirme, validasyon, hata yakalama, caching, audit logging, işlem yönetimi ve bunun gibi konular için eksiksiz altyapı.",
"BuiltInBundlingMinification": "Hazır Paketleme & Küçültme",
"BuiltInBundlingMinificationExplanation": "Paketleme ve küçültmek için external araçları kullanmayı bırakın. ABP daha basit, dinamik, güçlü, modüler ve hazır yolları öneriyor.",
"VirtualFileSystem": "Sanal Dosya Sistemi",
"VirtualFileSystemExplanation": "Sayfaları, scriptleri, stilleri, resimleri... paketlere/kütüpanelere gömün ve farklı uygulamalarda yeniden kullanın. ",
"Theming": "Theming",
"ThemingExplanationShort": "Bootstrap tabanlı standart kullanıcı arayüzlerini kullan ve kişiselleştir veya kendin yeni bir tane oluştur.",
"BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dinamik Formlar",
"BootstrapTagHelpersDynamicFormsExplanation": "Bootstrap komponentlerinin tekrar eden detaylarını manuel olarak yazmak yerine, Bu işlemi basitleştirmek ve iyileştirme avantajından faydalanmak için ABP'nin tag helperlarını kullanın. Dinamik form bir C# sınıfından model olarak eksiksik form oluşturabilir.",
"HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies",
"HTTPAPIsDynamicProxiesExplanation": "Application servislerini otomatik olarak Rest stil Http API olarak ayarlayın ve dinamaik Javascript & C# proxyler ile kullanın.",
"CompleteArchitectureInfo": "Bakım yapılabilir yazılım çözümleri üretmek için modern mimari.",
"DomainDrivenDesignBasedLayeringModelExplanation": "DDD tabanlı bir katmanlı mimari geliştirmek ve bakım yapılabilir bir kod altyapısı inşaa etmek için size yardım eder.",
"DomainDrivenDesignBasedLayeringModelExplanationCont": "DDD patern ve prensiplerinden yola çıkarak uygulamanızı geliştirmeye yardımcı olmak için başlanıç templateler, soyutlamalar, base sınıflar, servisler, dokümantasyon ve rehberlik sağlar. ",
"MicroserviceCompatibleModelExplanation": "Core framework & pre-build modüller mikroservis mimari göz önünde bulundurularak dizayn edildi.",
"MicroserviceCompatibleModelExplanationCont": "Microservice çözümlerini daha kolay geliştirmek için altyapı, entegrasyon, örnekler ve dokümantasyon sunarken eğer bir tek parça uygulama istiyorsanız ek karmaşıklık getirmez.",
"ModularInfo": "ABP yeniden kullanılabilir uygulama modülleri geliştirebilmenize izin veren eksiksiz modüler sistem sunar.",
"PreBuiltModulesThemes": "Pre-Built Modüller & Temalar",
"PreBuiltModulesThemesExplanation": "Açık kaynak ve ticari modüller & temalar iş uygulamanızda kullanıma hazırdır.",
"NuGetNPMPackages": "NUGET & NPM Packages",
"NuGetNPMPackagesExplanation": "NUGET & NPM paketleri olarak dağıtılmıştır. Yüklemek ve güncellemek kolaydır.",
"ExtensibleReplaceable": "Genişletilebilir/Değiştirilebilir",
"ExtensibleReplaceableExplanation": "Tüm sevisler & modüller genişletilebilirlik göz önünde bulundurularak dizayn edildi. Servislerin, sayfaların stillerin, komponentlerin vb. yerlerini değiştirebilirsizinz.",
"CrossCuttingConcernsExplanation2": "Kodunu daha temiz tut ve kendi uygulama koduna odaklan.",
"CrossCuttingConcernsExplanation3": "Ortak uygulama isterlerini tekrar ve tekrar geliştirmek için zaman harcamayın.",
"AuthenticationAuthorization": "Kimlik Doğrulama & Yetkilendirme",
"ExceptionHandling": "Hata yakalama",
"Validation": "Validasyon",
"DatabaseConnection": "Veritabanı bağlantısı",
"TransactionManagement": "İşlem yönetimi",
"AuditLogging": "Audit Logging",
"Caching": "Caching",
"Multitenancy": "Multitenancy",
"DataFiltering": "Date filtreleme",
"ConventionOverConfiguration": "Yapılandırma Üzerinde Kurallar",
"ConventionOverConfigurationExplanation": "ABP minimal veya sıfır yapılandırma ile ortak uygulama kurallarını varsayılan olarak uygular.",
"ConventionOverConfigurationExplanationList1": "Dependency injection için bilinen servisler otomatik olarak kaydedilir.",
"ConventionOverConfigurationExplanationList2": "Application servisler isimlerdirme kuralları ile HTTP API ler olarak uygulanır.",
"ConventionOverConfigurationExplanationList3": "C# ve JavaScript için dinamik HTTP istemci proxyleri yaratır.",
"ConventionOverConfigurationExplanationList4": "Entityleriniz için varsayılaan repositoriler sunar.",
"ConventionOverConfigurationExplanationList5": "Her web request veya application servis metodu için Unit of Work işlemini yönetir.",
"ConventionOverConfigurationExplanationList6": "Entityleriniz için oluşturma, güncelleme & silme işlemlerini yayınlar.",
"BaseClasses": "Ana Sınıflar",
"BaseClassesExplanation": "Ortak uygulama paternleri için pre-built ana sınıflar.",
"DeveloperFocusedExplanation": "ABP developerlar içindir.",
"DeveloperFocusedExplanationCont": "İhtiyacınız olduğunda düşük seviyede çalışmanızı kısıtlamadan günlük yazılım geliştirmenizi basitleştirmeyi amaçlar.",
"SeeAllFeatures": "Tüm Özellikleri Görüntüle",
"CLI_CommandLineInterface": "CLI (Command Line Interface)",
"CLI_CommandLineInterfaceExplanation": "CLI yeni proje oluşturma ve uygulamanıza modüller ekleme işlemlerini otomatik hale getirir.",
"StartupTemplates": "Başlangıç Templateler",
"StartupTemplatesExplanation": "Çeşitli başlangıç templateleri size geliştirme başlatmak için tam yapılandırılmış bir çözüm sağlar.",
"BasedOnFamiliarTools": "Bilinen Araçlara Dayalı ",
"BasedOnFamiliarToolsExplanation": "Zaten bildiğiniz popüler araçlar ile geliştirilme ve egtegre edilmiştir. Düşük öğrenme eğrisi, koaly adaptasyon, rahat geliştirme.",
"ORMIndependent": "ORM Bağımsız",
"ORMIndependentExplanation": "",
"Features": "ABP Framework Özelliklerini Keşfet",
"ABPCLI": "ABP CLI",
"Modularity": "Modülerlik",
"BootstrapTagHelpers": "Bootstrap Tag Helpers",
"DynamicForms": "Dinamik Formlar",
"BundlingMinification": "Paketleme & Küçültme",
"BackgroundJobs": "Arkaplan İşleri",
"DDDInfrastructure": "DDD altyapısı",
"DomainDrivenDesignInfrastructure": "Domain Driven Design Altyapısı",
"AutoRESTAPIs": "Otomatik REST APIler",
"DynamicClientProxies": "Dinamik Client Proxies",
"DistributedEventBus": "Dağıtılmış Event Bus",
"DistributedEventBusWithRabbitMQIntegration": "RabbitMQ Entegrasyonu ile Dağıtılmış Event Bus",
"TestInfrastructure": "Test ALtyapısı",
"AuditLoggingEntityHistories": "Audit Logging & Entity Histories",
"ObjectToObjectMapping": "Object to Object Mapping",
"EmailSMSAbstractions": "E-Posta & SMS Soyutlamaları",
"EmailSMSAbstractionsWithTemplatingSupport": "Template Destekli E-Posta & SMS Soyutlamaları",
"Localization": "Localization",
"SettingManagement": "Ayar Yönetimi",
"ExtensionMethods": "Extension Methods",
"ExtensionMethodsHelpers": "Extension Methods & Helpers",
"AspectOrientedProgramming": "Aspect Oriented Programming",
"DependencyInjection": "Dependency Injection",
"DependencyInjectionByConventions": "Dependency Injection by Conventions",
"ABPCLIExplanation": "ABP CLI (Command Line Interface) ABP tabanlı çözümler ortak işlemleri gerçekleştiren bir komut satırı aracıdır.",
"ModularityExplanation": "ABP, entityleri, servisleri, veritabanı entegrasyonu, APIleri, UI komponentleri ve bunun gibi özelliklere sahip olabilecek kendi uygulama modüllerini geliştirmeniz için eksiksiz bir altyapı sağlar. ",
"MultiTenancyExplanation": "ABP framework sadece multi-tenant uygulama geliştirmenizi desteklemekle kalmaz aynı zamanda kodunuzun çoğunlukla tenantların birbirinden haberi olmaycak şekilde olmasını sağlar.",
"MultiTenancyExplanation2": "Anlık tenant'ı otomatik olarak belirleybilir, farklı tenantların verilerini birbirlerinden izole edebilir.",
"MultiTenancyExplanation3": "Tek bir veritabanını, her tenant için ayrı bir veritabanını ve hibrid yaklaşımları destekler.",
"MultiTenancyExplanation4": "Sen kendi uygulama kodunu odaklan ve bırak framework sizin adınıza multi-tenancy üstesinden gelsin.",
"BootstrapTagHelpersExplanation": "Bootstrap komponentlerinin tekrar eden detaylarını manuel olarak yazmak yerine, Bu işlemi basitleştirmek ve iyileştirme avantajından faydalanmak için ABP'nin tag helperlarını kullanın. Dinamik form bir C# sınıfından model olarak eksiksik form oluşturabilir.",
"DynamicFormsExplanation": "Dinamik form & input tag helpers bir C# sınıfından model olarak eksiksik form oluşturabilir.",
"AuthenticationAuthorizationExplanation": "ASP.NET Core Identity & IdentityServer4 ile entegre edilmiş zengin kimlik doğrulama ve yetkilendirme opsiyonları. Genişletilebilir ve detaylandırılabilr bir izin sistemi sunar.",
"CrossCuttingConcernsExplanation": "Tüm bu ortak şeyleri geliştirmek için kendini sürekli tekrar etme. Kendi iş koduna odaklan ve bırak ABP bunları kurallar ile otomatik hale getirsin.",
"DatabaseConnectionTransactionManagement": "Veritabanı Bğlantısı & İşlem Yönetimi",
"CorrelationIdTracking": "Correlation-Id Tracking",
"BundlingMinificationExplanation": "ABP daha basit, dinamik, güçlü, modüler ve hazır paketlenmiş ve küçültülmüş sistemi öneriyor.",
"VirtualFileSystemnExplanation": "Sanal Dosya Sistemi fiziksel olarak disk üzerinde var olmayan dosyalarını yönetmeyi mümkün kılmaktadır. Bunlar genellikle önceden assemblyler içerisinde gömülü olan(js,css,image,cshtml..) dosyalardır ve bunlar fiziksel dosylar gibi runtimeda kullanılır.",
"ThemingExplanation": "Theming sistem son Bootstrap Framework tabanlı ortak bir kütüphane ve layout tanımlayarak uygulamanızı & modüllerini bağımsız olarak geliştirmenizi sağlamaktadır.",
"DomainDrivenDesignInfrastructureExplanation": "Domain Driven Design pattern ve prensiplerine dayalı katmanlı uygulama geliştirmek için eksiksiz bit altyapı",
"Specification": "Specification",
"Repository": "Repository",
"DomainService": "Domain Service",
"ValueObject": "Value Object",
"ApplicationService": "Application Service",
"DataTransferObject": "Data Transfer Object",
"AggregateRootEntity": "Aggregate Root, Entity",
"AutoRESTAPIsExplanation": "ABP, application servislerinizi otomatik olarak API Controller olarak kurallı bir şekilde yapılandırabilir.",
"DynamicClientProxiesExplanation": "Apilerinizi, JavaScript ve C# clients tarafından kolaylıkla kullanın.",
"DistributedEventBusWithRabbitMQIntegrationExplanation": "Easily publish & consume distributed events using built-in Distributed Event Bus with RabbitMQ integration available.",
"TestInfrastructureExplanation": "The framework has been developed unit & integration testing in mind. Provides you base classes to make it easier. Startup templates come with pre-configured for testing.",
"AuditLoggingEntityHistoriesExplanation": "",
"EmailSMSAbstractionsWithTemplatingSupportExplanation": "",
"LocalizationExplanation": "",
"SettingManagementExplanation": "",
"ExtensionMethodsHelpersExplanation": "",
"AspectOrientedProgrammingExplanation": "",
"DependencyInjectionByConventionsExplanation": "",
"DataFilteringExplanation": "",
"PublishEvents": "Publish Events",
"HandleEvents": "Handle Events",
"AndMore": "",
"Code": "Code",
"Result": "Sonuç",
"SeeTheDocumentForMoreInformation": "See the <a href=\"{1}\">{0} document</a> for more information",
"IndexPageHeroSection": "<span class=\"first-line shine\"><strong>open source</strong></span><span class=\"second-line text-uppercase\">Web Application<br />Framework </span><span class=\"third-line shine2\"><strong>for asp.net core</strong></span>",
"UiFramework": "UI Framework",
"EmailAddress": "E-Posta Adresi",
"Mobile": "Mobil",
"ReactNative": "React Native"
}
}

2
common.props

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

6
configureawait.props

@ -1,8 +1,8 @@
<Project>
<ItemGroup>
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" />
<PackageReference Include="Fody" Version="6.0.6">
<PrivateAssets>all</PrivateAssets>
<PackageReference Include="ConfigureAwait.Fody" Version="3.3.1" PrivateAssets="All" />
<PackageReference Include="Fody" Version="6.2.0">
<PrivateAssets>All</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

2
docs/en/Blob-Storing.md

@ -169,7 +169,7 @@ public class ProfileAppService : ApplicationService
If you don't use the generic argument and directly inject the `IBlobContainer` (as explained before), you get the default container. Another way of injecting the default container is using `IBlobContainer<DefaultContainer>`, which returns exactly the same container.
The name of the default container is `Default`.
The name of the default container is `default`.
### Named Containers

1
docs/en/CLI.md

@ -115,6 +115,7 @@ abp update [options]
* `--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.
* `--check-all`: Check the new version of each package separately. Default is `false`.
### add-package

133
docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md

@ -1,3 +1,134 @@
# Distributed Event Bus RabbitMQ Integration
TODO
> This document explains **how to configure the [RabbitMQ](https://www.rabbitmq.com/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system
## Installation
Use the ABP CLI to add [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet package to your project:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.RabbitMQ` package.
* Run `abp add-package Volo.Abp.EventBus.RabbitMQ` command.
If you want to do it manually, install the [Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusRabbitMqModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
## Configuration
You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes.
### `appsettings.json` file configuration
This is the simplest way to configure the RabbitMQ settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/).
**Example: The minimal configuration to connect to a local RabbitMQ server with default configurations**
````json
{
"RabbitMQ": {
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName"
}
}
}
````
* `ClientName` is the name of this application, which is used as the **queue name** on the RabbitMQ.
* `ExchangeName` is the **exchange name**.
See [the RabbitMQ document](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues) to understand these options better.
#### Connections
If you need to connect to another server than the localhost, you need to configure the connection properties.
**Example: Specify the host name (as an IP address)**
````json
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123"
}
},
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName"
}
}
}
````
Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus.
**Example: Declare two connections and use one of them for the event bus**
````json
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123"
},
"SecondConnection": {
"HostName": "321.321.321.321"
}
},
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName",
"ConnectionName": "SecondConnection"
}
}
}
````
This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus.
You can use any of the [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) properties as the connection properties.
**Example: Specify the connection port**
````csharp
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123",
"Port": "5672"
}
}
}
}
````
### The Options Classes
`AbpRabbitMqOptions` and `AbpRabbitMqEventBusOptions` classes can be used to configure the connection strings and event bus options for the RabbitMQ.
You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md).
**Example: Configure the connection**
````csharp
Configure<AbpRabbitMqOptions>(options =>
{
options.Connections.Default.UserName = "user";
options.Connections.Default.Password = "pass";
options.Connections.Default.HostName = "123.123.123.123";
options.Connections.Default.Port = 5672;
});
````
**Example: Configure the client and exchange names**
````csharp
Configure<AbpRabbitMqEventBusOptions>(options =>
{
options.ClientName = "TestApp1";
options.ExchangeName = "TestMessages";
});
````
Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file.

298
docs/en/Distributed-Event-Bus.md

@ -1,3 +1,299 @@
# Distributed Event Bus
TODO
Distributed Event bus system allows to **publish** and **subscribe** to events that can be **transferred across application/service boundaries**. You can use the distributed event bus to asynchronously send and receive messages between **microservices** or **applications**.
## Providers
Distributed event bus system provides an **abstraction** that can be implemented by any vendor/provider. There are two providers implemented out of the box:
* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider.
* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it.
Using a local event bus as default has a few important advantages. The most important one is that: It allows you to write your code compatible to distributed architecture. You can write a monolithic application now that can be split into microservices later. It is a good practice to communicate between bounded contexts (or between application modules) via distributed events instead of local events.
For example, [pre-built application modules](Modules/Index.md) is designed to work as a service in a distributed system while they can also work as a module in a monolithic application without depending an external message broker.
## Publishing Events
There are two ways of publishing distributed events explained in the following sections.
### IDistributedEventBus
`IDistributedEventBus` can be [injected](Dependency-Injection.md) and used to publish a distributed event.
**Example: Publish a distributed event when the stock count of a product changes**
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly IDistributedEventBus _distributedEventBus;
public MyService(IDistributedEventBus distributedEventBus)
{
_distributedEventBus = distributedEventBus;
}
public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
{
await _distributedEventBus.PublishAsync(
new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
}
);
}
}
}
````
`PublishAsync` method gets a single parameter: the event object, which is responsible to hold the data related to the event. It is a simple plain class:
````csharp
using System;
namespace AbpDemo
{
[EventName("MyApp.Product.StockChange")]
public class StockCountChangedEto
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
}
````
Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case).
> `Eto` is a suffix for **E**vent **T**ransfer **O**bjects we use by convention. While it is not required, we find it useful to identify such event classes (just like [DTOs](Data-Transfer-Objects.md) on the application layer).
#### Event Name
`EventName` attribute is optional, but suggested. If you don't declare it, the event name will be the full name of the event class, `AbpDemo.StockCountChangedEto` in this case.
#### About Serialization for the Event Objects
Event transfer objects **must be serializable** since they will be serialized/deserialized to JSON or other format when it is transferred to out of the process.
Avoid circular references, polymorphism, private setters and provide default (empty) constructors if you have any other constructor as a good practice (while some serializers may tolerate it), just like the DTOs.
### Inside Entity / Aggregate Root Classes
[Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish distributed events inside entity / aggregate root classes.
**Example: Publish a distributed event inside an aggregate root method**
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace AbpDemo
{
public class Product : AggregateRoot<Guid>
{
public string Name { get; set; }
public int StockCount { get; private set; }
private Product() { }
public Product(Guid id, string name)
: base(id)
{
Name = name;
}
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
//ADD an EVENT TO BE PUBLISHED
AddDistributedEvent(
new StockCountChangedEto
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
````
`AggregateRoot` class defines the `AddDistributedEvent` to add a new distributed event, that is published when the aggregate root object is saved (created, updated or deleted) into the database.
> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event.
#### IGeneratesDomainEvents Interface
Actually, adding distributed events are not unique to the `AggregateRoot` class. You can implement `IGeneratesDomainEvents` for any entity class. But, `AggregateRoot` implements it by default and makes it easy for you.
> It is not suggested to implement this interface for entities those are not aggregate roots, since it may not work for some database providers for such entities. It works for EF Core, but not works for MongoDB for example.
#### How It Was Implemented?
Calling the `AddDistributedEvent` doesn't immediately publish the event. The event is published when you save changes to the database;
* For EF Core, it is published on `DbContext.SaveChanges`.
* For MongoDB, it is published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system).
## Subscribing to Events
A service can implement the `IDistributedEventHandler<TEvent>` to handle the event.
**Example: Handle the `StockCountChangedEto` defined above**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyHandler
: IDistributedEventHandler<StockCountChangedEto>,
ITransientDependency
{
public async Task HandleEventAsync(StockCountChangedEto eventData)
{
var productId = eventData.ProductId;
}
}
}
````
That's all.
* `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEto` event occurs.
* If you are using a distributed message broker, like RabbitMQ, ABP automatically **subscribes to the event on the message broker**, gets the message, executes the handler.
* It sends **confirmation (ACK)** to the message broker if the event handler was successfully executed (did not throw any exception).
You can inject any service and perform any required logic here. A single event handler class can **subscribe to multiple events** but implementing the `IDistributedEventHandler<TEvent>` interface for each event type.
> The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options.
## Pre-Defined Events
ABP Framework **automatically publishes** distributed events for **create, update and delete** operations for an [entity](Entities.md) once you configure it.
### Event Types
There are three pre-defined event types:
* `EntityCreatedEto<T>` is published when an entity of type `T` was created.
* `EntityUpdatedEto<T>` is published when an entity of type `T` was updated.
* `EntityDeletedEto<T>` is published when an entity of type `T` was deleted.
These types are generics. `T` is actually the type of the **E**vent **T**ransfer **O**bject (ETO) rather than the type of the entity. Because, an entity object can not be transferred as a part of the event data. So, it is typical to define a ETO class for an entity class, like `ProductEto` for `Product` entity.
### Subscribing to the Events
Subscribing to the auto events is same as subscribing a regular distributed event.
**Example: Get notified once a product updated**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyHandler :
IDistributedEventHandler<EntityUpdatedEto<ProductEto>>,
ITransientDependency
{
public async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData)
{
var productId = eventData.Entity.Id;
//TODO
}
}
}
````
* `MyHandler` implements the `IDistributedEventHandler<EntityUpdatedEto<ProductEto>>`.
### Configuration
You can configure the `AbpDistributedEntityEventOptions` in the `ConfigureServices` of your [module](Module-Development-Basics.md) to add a selector.
**Example: Configuration samples**
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
//Enable for all entities
options.AutoEventSelectors.AddAll();
//Enable for a single entity
options.AutoEventSelectors.Add<IdentityUser>();
//Enable for all entities in a namespace (and child namespaces)
options.AutoEventSelectors.AddNamespace("Volo.Abp.Identity");
//Custom predicate expression that should return true to select a type
options.AutoEventSelectors.Add(
type => type.Namespace.StartsWith("MyProject.")
);
});
````
* The last one provides flexibility to decide if the events should be published for the given entity type. Returns `true` to accept a `Type`.
You can add more than one selector. If one of the selectors match for an entity type, then it is selected.
### Event Transfer Object
Once you enable **auto events** for an entity, ABP Framework starts to publish events on the changes on this entity. If you don't specify a corresponding **E**vent **T**ransfer **O**bject (ETO) for the entity, ABP Framework uses a standard type, named `EntityEto`, which has only two properties:
* `EntityType` (`string`): Full name (including namespace) of the entity class.
* `KeysAsString` (`string`): Primary key(s) of the changed entity. If it has a single key, this property will be the primary key value. For a composite key, it will contain all keys separated by `,` (comma).
So, you can implement the `IDistributedEventHandler<EntityUpdatedEto<EntityEto>>` to subscribe the events. However, it is not a good approach to subscribe to such a generic event. You can define the corresponding ETO for the entity type.
**Example: Declare to use `ProductEto` for the `Product` entity**
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
options.AutoEventSelectors.Add<Product>();
options.EtoMappings.Add<Product, ProductEto>();
});
````
This example;
* Adds a selector to allow to publish the create, update and delete events for the `Product` entity.
* Configure to use the `ProductEto` as the event transfer object to publish for the `Product` related events.
Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the mapping. You can check the object to object mapping document for all options, but the following example shows how to configure it with the [AutoMapper](https://automapper.org/) library.
**Example: Configure `Product` to `ProductEto` mapping using the AutoMapper**
````csharp
using System;
using AutoMapper;
using Volo.Abp.Domain.Entities.Events.Distributed;
namespace AbpDemo
{
[AutoMap(typeof(Product))]
public class ProductEto : EntityEto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
}
````
This example uses the `AutoMap` attribute of the AutoMapper to configure the mapping. You could create a profile class instead. Please refer to the AutoMapper document for more options.

63
docs/en/Entity-Framework-Core-Oracle-Devart.md

@ -0,0 +1,63 @@
# Switch to EF Core Oracle Devart Provider
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
> This document uses a paid library of [Devart](https://www.devart.com/dotconnect/oracle/) company, See [this document](Entity-Framework-Core-Oracle.md) for other options.
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleDevartModule)`
Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`.
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.

58
docs/en/Entity-Framework-Core-Oracle-Official.md

@ -0,0 +1,58 @@
# Switch to EF Core Oracle Provider
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleModule)`
Also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle;`.
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one (just changes `UseSqlServer(...)` to `UseOracle(...)`)
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseOracle(configuration.GetConnectionString("Default"));
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.

60
docs/en/Entity-Framework-Core-Oracle.md

@ -2,61 +2,9 @@
This document explains how to switch to the **Oracle** database provider for **[the application startup template](Startup-Templates/Application.md)** which comes with SQL Server provider pre-configured.
> This document uses a paid library of [Devart](https://www.devart.com/dotconnect/oracle/) company, because it is the only library for Oracle that supports EF Core 3.x.
ABP Framework provides integrations for two different Oracle packages. See one of the following documents based on your provider decision:
## Replace the Volo.Abp.EntityFrameworkCore.SqlServer Package
`.EntityFrameworkCore` project in the solution depends on the [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet package. Remove this package and add the same version of the [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) package.
## Replace the Module Dependency
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
with this one
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> Depending on your solution structure, you may find more code files need to be changed.
## Change the Connection Strings
Oracle connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com]( https://www.connectionstrings.com/oracle/ ) for details of Oracle connection string options.
You typically will change the `appsettings.json` inside the `.DbMigrator` and `.Web` projects, but it depends on your solution structure.
## Re-Generate the Migrations
The startup template uses [Entity Framework Core's Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) by default.
EF Core Migrations depend on the selected DBMS provider. Changing the DBMS provider, may not work with the existing migrations.
* Delete the `Migrations` folder under the `.EntityFrameworkCore.DbMigrations` project and re-build the solution.
* Run `Add-Migration "Initial"` on the Package Manager Console window (select the `.DbMigrator` (or `.Web`) project as the startup project in the Solution Explorer and select the `.EntityFrameworkCore.DbMigrations` project as the default project in the Package Manager Console).
This will scaffold a new migration for Oracle.
Run the `.DbMigrator` project to create the database, apply the changes and seed the initial data.
## Run the Application
It is ready. Just run the application and enjoy coding.
* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** package uses the official & free oracle driver (which is **currently in beta**).
* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** package uses the commercial (paid) driver of [Devart](https://www.devart.com/) company.
> You can choose one of the package you want. If you don't know the differences of the packages, please search for it. ABP Framework only provides integrations it doesn't provide support for such 3rd-party libraries.

9
docs/en/Event-Bus.md

@ -1,3 +1,10 @@
# Event Bus
TODO
An event bus is a mediator that transfers a message from a sender to a receiver. In this way, it provides a loosely coupled communication way between objects, services and applications.
## Event Bus Types
ABP Framework provides two type of event buses;
* **[Local Event Bus](Local-Event-Bus.md)** is suitable for in-process messaging.
* **[Distributed Event Bus](Distributed-Event-Bus.md)** is suitable for inter-process messaging, like microservices publishing and subscribing to distributed events.

72
docs/en/Getting-Started-React-Native.md

@ -0,0 +1,72 @@
# Getting Started with the React Native
ABP platform provide basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated to your ABP based backends**.
When you **create a new application** as described in the [getting started document](Getting-Started.md), the solution includes the React Native application in the `react-native` folder as default.
## Configure Your Local IP Address
A React Native application running on an Android emulator or a physical phone **can not connect to the backend** on `localhost`. To fix this problem, it is necessary to run the backend application on your **local IP address**.
{{ 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 Server Application
Run the backend application as described in the [getting started document](Getting-Started.md).
> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the 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):
```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` or `.Web` project.
{{else}}
> Make sure that `issuer` and `apiUrl` matches the running address of the `.HttpApi.Host` or `.Web` 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/workflow/android-simulator/), [iOS Simulator](https://docs.expo.io/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.

146
docs/en/Getting-Started.md

@ -1,4 +1,4 @@
# Getting started
# Getting Started
````json
//[doc-params]
@ -9,36 +9,36 @@
}
````
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.
This tutorial explains how to create a new {{if UI == "MVC"}} ASP.NET Core MVC (Razor Pages) web {{else if UI == "NG"}} Angular {{end}} application using the startup template.
## Setup your development environment
## Setup Your Development Environment
First things first! Let's setup your development environment before creating the first project.
### Pre-requirements
### 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/)
* [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.1+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
{{ if Tiered == "Yes" }}
* [Redis](https://redis.io/): The applications use Redis as as [distributed cache](../Caching.md). So, you need to have Redis installed & running.
* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)).
{{ end }}
> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core.
> *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](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based applications.
> ABP CLI is a free & open source tool for [the ABP framework](https://abp.io/).
> ABP CLI is a free & open source tool for the ABP framework.
First, you need to install the ABP CLI using the following command:
@ -52,27 +52,25 @@ If you've already installed, you can update it using the following command:
dotnet tool update -g Volo.Abp.Cli
````
## Create a new project
## 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
### 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}}
abp new Acme.BookStore{{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.
* `--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 on the server.
{{ end }}
@ -92,11 +90,13 @@ abp new Acme.BookStore -t app{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mon
> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.
#### ABP CLI commands & options
#### ABP CLI Commands & Options
[ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. This document uses the [application startup template](Startup-Templates/Application.md) to create a new web application. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates.
[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.
> Alternatively, you can select the "Direct Download" tab from the [ABP Framework web site](https://abp.io/get-started) to create a new solution.
## The solution structure
## The Solution Structure
{{ if UI == "MVC" }}
@ -135,7 +135,7 @@ Open the `.sln` (Visual Studio solution) file under the `aspnet-core` folder:
>
> 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.
The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and also contains unit & integration test projects.
{{ if DB == "EF" }}
@ -149,9 +149,9 @@ Integration tests projects are properly configured to work with in-memory **Mong
> See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details.
## Create the database
## Create the Database
### Database connection string
### 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}}:
@ -163,13 +163,13 @@ Check the **connection string** in the `appsettings.json` file under the {{if UI
}
````
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.
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](Entity-Framework-Core-Other-DBMS.md).
### Apply the migrations
### 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
#### 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.
@ -183,9 +183,9 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![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.
> Initial [seed data](Data-Seeding.md) 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
#### Using EF Core Update-Database Command
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations.
@ -207,7 +207,9 @@ Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations`
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.
> **Using the `.DbMigrator` tool is the suggested way**, because it also seeds the initial data to be able to properly run the web application.
>
> If you just use the `Update-Database` command, you will have an empty database, so you can not login to the application since there is no initial admin user in the database. You can use the `Update-Database` command in development time when you don't need to seed the database. However, using the `.DbMigrator` application is easier and you can always use it to migrate the schema and seed the database.
{{ else if DB == "Mongo" }}
@ -219,7 +221,7 @@ This will create a new database based on the configured connection string.
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
### 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.
@ -233,11 +235,11 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![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.
> Initial [seed data](Data-Seeding.md) 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
## Run the Application
{{ if UI == "MVC" }}
@ -320,7 +322,6 @@ Once all node modules are loaded, execute `yarn start` (or `npm start`) command:
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/)
@ -331,83 +332,18 @@ open your web browser and navigate to [localhost:4200](http://localhost:4200/)
{{ 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).
Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template.
> See the [Android Studio Emulator](https://docs.expo.io/workflow/android-simulator/), [iOS Simulator](https://docs.expo.io/workflow/ios-simulator/) documents on expo.io.
## Mobile Development
![React Native login screen on iPhone 11](images/rn-login-iphone.png)
When you create a new application, the solution includes `react-native` folder by default. This is a basic [React Native](https://reactnative.dev/) startup template to develop mobile applications integrated to your ABP based backends.
Enter **admin** as the username and **1q2w3E*** as the password to login to the application.
If you don't plan to develop a mobile application with React Native, you can safely delete the `react-native` folder.
The application is up and running. You can continue to develop your application based on this startup template.
> You can specifying the `--mobile none` option to the ABP CLI to not create the `react-native` folder in the beginning.
> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules.
See the "[Getting Started with the React Native](Getting-Started-React-Native.md)" document to learn how to configure and run the React Native application.
## What's next?
## See Also
[Application development tutorial](Tutorials/Part-1.md)
* [Web Application Development Tutorial](Tutorials/Part-1.md)

6
docs/en/Index.md

@ -1,6 +1,6 @@
# ABP Documentation
ABP is an **open source application framework** focused on ASP.NET Core based web application development, but also supports developing other types of applications.
ABP is an **open source application framework** focused on ASP.NET Core based web application development, but also supports developing other type of applications.
Explore the left navigation menu to deep dive in the documentation.
@ -16,6 +16,10 @@ If you want to start from scratch (with an empty project) then manually install
* [Console Application](Getting-Started-Console-Application.md)
* [ASP.NET Core Web Application](Getting-Started-AspNetCore-Application.md)
## Packages
ABP Framework is distributed as NuGet and NPM packages. See [this page](http://abp.io/packages) for the complete list of the packages.
## Source Code
ABP is hosted on GitHub. See [the source code](https://github.com/abpframework/abp).

226
docs/en/Local-Event-Bus.md

@ -1,3 +1,227 @@
# Local Event Bus
TODO
The Local Event Bus allows services to publish and subscribe to **in-process events**. That means it is suitable if two services (publisher and subscriber) are running in the same process.
## Publishing Events
There are two ways of publishing local events explained in the following sections.
### ILocalEventBus
`ILocalEventBus` can be [injected](Dependency-Injection.md) and used to publish a local event.
**Example: Publish a local event when the stock count of a product changes**
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
public MyService(ILocalEventBus localEventBus)
{
_localEventBus = localEventBus;
}
public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
{
//TODO: IMPLEMENT YOUR LOGIC...
//PUBLISH THE EVENT
await _localEventBus.PublishAsync(
new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
}
);
}
}
}
````
`PublishAsync` method gets a single parameter: the event object, which is responsible to hold the data related to the event. It is a simple plain class:
````csharp
using System;
namespace AbpDemo
{
public class StockCountChangedEvent
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
}
````
Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case).
### Inside Entity / Aggregate Root Classes
[Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish local events inside entity / aggregate root classes.
**Example: Publish a local event inside an aggregate root method**
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace AbpDemo
{
public class Product : AggregateRoot<Guid>
{
public string Name { get; set; }
public int StockCount { get; private set; }
private Product() { }
public Product(Guid id, string name)
: base(id)
{
Name = name;
}
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
//ADD an EVENT TO BE PUBLISHED
AddLocalEvent(
new StockCountChangedEvent
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
````
`AggregateRoot` class defines the `AddLocalEvent` to add a new local event, that is published when the aggregate root object is saved (created, updated or deleted) into the database.
> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event.
#### IGeneratesDomainEvents Interface
Actually, adding local events are not unique to the `AggregateRoot` class. You can implement `IGeneratesDomainEvents` for any entity class. But, `AggregateRoot` implements it by default and makes it easy for you.
> It is not suggested to implement this interface for entities those are not aggregate roots, since it may not work for some database providers for such entities. It works for EF Core, but not works for MongoDB for example.
#### How It Was Implemented?
Calling the `AddLocalEvent` doesn't immediately publish the event. The event is published when you save changes to the database;
* For EF Core, it is published on `DbContext.SaveChanges`.
* For MongoDB, it is published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system).
## Subscribing to Events
A service can implement the `ILocalEventHandler<TEvent>` to handle the event.
**Example: Handle the `StockCountChangedEvent` defined above**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
namespace AbpDemo
{
public class MyHandler
: ILocalEventHandler<StockCountChangedEvent>,
ITransientDependency
{
public async Task HandleEventAsync(StockCountChangedEvent eventData)
{
//TODO: your code that does somthing on the event
}
}
}
````
That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic here.
* **Zero or more handlers** can subscribe to the same event.
* A single event handler class can **subscribe to multiple events** but implementing the `ILocalEventHandler<TEvent>` interface for each event type.
> The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options.
## Transaction & Exception Behavior
When an event published, subscribed event handlers are immediately executed. So;
* If a handler **throws an exception**, it effects the code that published the event. That means it gets the exception on the `PublishAsync` call. So, **use try-catch yourself** in the event handler if you want to hide the error.
* If the event publishing code is being executed inside a [Unit Of Work](Unit-Of-Work.md) scope, the event handlers also covered by the unit of work. That means if your UOW is transactional and a handler throws an exception, the transaction is rolled back.
## Pre-Built Events
It is very common to **publish events on entity create, update and delete** operations. ABP Framework **automatically** publish these events for all entities. You can just subscribe to the related event.
**Example: Subscribe to an event that published when a user was created**
````csharp
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
namespace AbpDemo
{
public class MyHandler
: ILocalEventHandler<EntityCreatedEventData<IdentityUser>>,
ITransientDependency
{
public async Task HandleEventAsync(
EntityCreatedEventData<IdentityUser> eventData)
{
var userName = eventData.Entity.UserName;
var email = eventData.Entity.Email;
//...
}
}
}
````
This class subscribes to the `EntityCreatedEventData<IdentityUser>`, which is published just after a user was created. You may want to send a "Welcome" email to the new user.
There are two types of these events: events with past tense and events with continuous tense.
### Events with Past Tense
Events with past tense are published when the related unit of work completed and the entity change successfully saved to the database. If you throw an exception on these event handlers, it **can not rollback** the transaction since it was already committed.
The event types are;
* `EntityCreatedEventData<T>` is published just after an entity was successfully created.
* `EntityUpdatedEventData<T>` is published just after an entity was successfully updated.
* `EntityDeletedEventData<T>` is published just after an entity was successfully deleted.
* `EntityChangedEventData<T>` is published just after an entity was successfully created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events.
### Events with Continuous Tense
Events with continuous tense are published before completing the transaction (if database transaction is supported by the database provider being used). If you throw an exception on these event handlers, it **can rollback** the transaction since it is not completed yet and the change is not saved to the database.
The event types are;
* `EntityCreatingEventData<T>` is published just before saving a new entity to the database.
* `EntityUpdatingEventData<T>` is published just before an existing entity is being updated.
* `EntityDeletingEventData<T>` is published just before an entity is being deleted.
* `EntityChangingEventData<T>` is published just before an entity is being created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events.
#### How It Was Implemented?
Pre-build events are published when you save changes to the database;
* For EF Core, they are published on `DbContext.SaveChanges`.
* For MongoDB, they are published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system).

2
docs/en/Modules/Identity.md

@ -1,4 +1,6 @@
# Identity Management Module
Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library.
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...

2
docs/en/Modules/Index.md

@ -17,7 +17,7 @@ There are some **free and open source** application modules developed and mainta
* **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module.
* [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module.
* **Feature Management**: Used to persist and manage the [features](../Features.md).
* **Identity**: Manages roles, users and their permissions, based on the Microsoft Identity library.
* **[Identity](Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library.
* **IdentityServer**: Integrates to IdentityServer4.
* **Permission Management**: Used to persist permissions.
* **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md).

47
docs/en/Modules/Organization-Units.md

@ -0,0 +1,47 @@
# Organization Unit Management
Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**.
### OrganizationUnit Entity
An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are:
- **TenantId**: Tenant's Id of this OU. Can be null for host OUs.
- **ParentId**: Parent OU's Id. Can be null if this is a root OU.
- **Code**: A hierarchical string code that is unique for a tenant.
- **DisplayName**: Shown name of the OU.
The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class.
#### Organization Tree
Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree;
- There can be more than one root (where the `ParentId` is `null`).
- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below).
#### OU Code
OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this:
"**00001.00042.00005**"
This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code:
- It must be **unique** for a [tenant](../Multi-Tenancy.md).
- All the children of the same OU have codes that **start with the parent OU's code**.
- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample.
- While the OU code is unique, it can be **changeable** if you move an OU.
- You must reference an OU by Id, not Code.
### OrganizationUnit Manager
The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are:
- Create, Update or Delete an OU
- Move an OU in the OU tree.
- Getting information about the OU tree and its items.
#### Multi-Tenancy
The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default.

3
docs/en/RabbitMq.md

@ -0,0 +1,3 @@
# RabbitMQ
TODO!

128
docs/en/Repositories.md

@ -38,6 +38,8 @@ public class PersonAppService : ApplicationService
}
````
> See the "*IQueryable & Async Operations*" section below to understand how you can use **async extension methods**, like `ToListAsync()` (which is strongly suggested) instead of `ToList()`.
In this example;
* `PersonAppService` simply injects `IRepository<Person, Guid>` in it's constructor.
@ -116,5 +118,129 @@ public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPe
}
````
You can directly access the data access provider (`DbContext` in this case) to perform operations. See [entity framework integration document](Entity-Framework-Core.md) for more about custom repositories based on EF Core.
You can directly access the data access provider (`DbContext` in this case) to perform operations.
> See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories.
## IQueryable & Async Operations
`IRepository` inherits from `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Generic Repositories*" section above.
**Example: Using the `Where(...)` and the `ToList()` extension methods**
````csharp
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToList();
````
`.ToList`, `Count()`... are standard extension methods defined in the `System.Linq` namespace ([see all](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)).
You normally want to use `.ToListAsync()`, `.CountAsync()`... instead, to be able to write a **truly async code**.
However, you see that you can't use these async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because;
* These async methods **are not standard LINQ methods** and they are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package.
* The standard template **doesn't have a reference** to the EF Core package from the domain and application layers, to be independent from the database provider.
Based on your requirements and development model, you have the following options to be able to use the async methods.
> Using async methods is strongly suggested! Don't use sync LINQ methods while executing database queries to be able to develop a scalable application.
### Option-1: Reference to the EF Core
The easiest solution is to directly add the EF Core package from the project you want to use these async methods.
> Add the [Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore) NuGet package to your project, which indirectly reference to the EF Core package. This ensures that you use the correct version of the EF Core compatible to the rest of your application.
When you add the NuGet package to your project, you can take full power of the EF Core extension methods.
**Example: Directly using the `ToListAsync()` after adding the EF Core package**
````csharp
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToListAsync();
````
This method is suggested;
* If you are developing an application and you **don't plan to change** EF Core in the future, or you can **tolerate** it if you need to change later. We believe that's reasonable if you are developing a final application.
#### MongoDB Case
If you are using [MongoDB](MongoDB.md), you need to add the [Volo.Abp.MongoDB](https://www.nuget.org/packages/Volo.Abp.MongoDB) NuGet package to your project. Even in this case, you can't directly use async LINQ extensions (like `ToListAsync`) because MongoDB doesn't provide async extension methods for `IQueryable<T>`, but provides for `IMongoQueryable<T>`. You need to cast the query to `IMongoQueryable<T>` first to be able to use the async extension methods.
**Example: Cast `IQueryable<T>` to `IMongoQueryable<T>` and use `ToListAsync()`**
````csharp
var people = ((IMongoQueryable<Person>)_personRepository
.Where(p => p.Name.Contains(nameFilter)))
.ToListAsync();
````
### Option-2: Custom Repository Methods
You can always create custom repository methods and use the database provider specific APIs, like async extension methods here. See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories.
This method is suggested;
* If you want to **completely isolate** your domain & application layers from the database provider.
* If you develop a **reusable [application module](Modules/Index.md)** and don't want to force to a specific database provider, which should be done as a [best practice](Best-Practices/Index.md).
### Option-3: IAsyncQueryableExecuter
`IAsyncQueryableExecuter` is a service that is used to execute an `IQueryable<T>` object asynchronously **without depending on the actual database provider**.
**Example: Inject & use the `IAsyncQueryableExecuter.ToListAsync()` method**
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly IAsyncQueryableExecuter _asyncExecuter;
public ProductAppService(
IRepository<Product, Guid> productRepository,
IAsyncQueryableExecuter asyncExecuter)
{
_productRepository = productRepository;
_asyncExecuter = asyncExecuter;
}
public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Create the query
var query = _productRepository
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);
//Run the query asynchronously
List<Product> products = await _asyncExecuter.ToListAsync(query);
//...
}
}
}
````
> `ApplicationService` and `DomainService` base classes already have `AsyncExecuter` properties pre-injected and usable without needing an explicit constructor injection.
ABP Framework executes the query asynchronously using the actual database provider's API. While that is not a usual way to execute a query, it is the best way to use the async API without depending on the database provider.
This method is suggested;
* If you are building a **reusable library** that doesn't have a database provider integration package, but needs to execute an `IQueryable<T>` object in some case.
For example, ABP Framework uses the `IAsyncQueryableExecuter` in the `CrudAppService` base class (see the [application services](Application-Services.md) document).

1
docs/en/Road-Map.md

@ -9,7 +9,6 @@ While we will **continue to add other exciting features**, we will work on the f
Beside this middle term goals, there are many features in the [backlog](https://github.com/abpframework/abp/milestone/2). Here, a list of some major items in the backlog;
* [#4098](https://github.com/abpframework/abp/issues/4098) / Blob Storing Azure provider.
* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a **gRPC integration** infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](https://docs.abp.io/en/abp/latest/Modules/Index))
* [#236](https://github.com/abpframework/abp/issues/236) Resource based authorization system
* [#1754](https://github.com/abpframework/abp/issues/1754) / Multi-lingual entities

4
docs/en/Samples/Index.md

@ -7,7 +7,7 @@ Here, a list of official samples built with the ABP Framework. Most of these sam
A complete solution to demonstrate how to build systems based on the microservice architecture.
* [The complete documentation for this sample](Microservice-Demo.md)
* [Source code](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo)
* [Source code](https://github.com/abpframework/abp-samples/tree/master/MicroserviceDemo)
* [Microservice architecture document](../Microservice-Architecture.md)
### Book Store
@ -48,6 +48,8 @@ While there is no Razor Pages & MongoDB combination, you can check both document
* **Text Templates Demo**: Shows different use cases of the text templating system.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo)
* [Text templating documentation](../Text-Templating.md)
* **Stored Procedure Demo**: Demonstrates how to use stored procedures, database views and functions with best practices.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/StoredProcedureDemo)
* **Authentication Customization**: A solution to show how to customize the authentication for ASP.NET Core MVC / Razor Pages applications.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)
* Related "[How To](../How-To/Index.md)" documents:

2
docs/en/Samples/Microservice-Demo.md

@ -28,7 +28,7 @@ The diagram below shows the system:
### Source Code
You can get the source code from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/MicroserviceDemo).
You can get the source code from [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/MicroserviceDemo).
## Running the Solution

3
docs/en/SignalR-Integration.md

@ -234,4 +234,5 @@ Refer to the Microsoft's documentation to [host and scale](https://docs.microsof
## See Also
* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)
* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction)
* [Real-Time Messaging In A Distributed Architecture Using ABP, SingalR & RabbitMQ](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ)

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

@ -0,0 +1,248 @@
# Web Application Development Tutorial - Part 4: Integration Tests
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"DB": ["EF","Mongo"]
}
````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## About This Tutorial
In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies:
* **{{DB_Text}}** as the ORM provider.
* **{{UI_Value}}** as the UI Framework.
This tutorial is organized as the following parts;
- [Part 1: Creating the project and book list page](Part-1.md)
- [Part 2: The book list page](Part-2.md)
- [Part 3: Creating, updating and deleting books](Part-3.md)
- **Part 4: Integration tests (this part)**
- [Part 5: Authorization](Part-5.md)
### Download the Source Code
This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded:
* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
## Test Projects in the Solution
This part covers the **server side** tests. There are several test projects in the solution:
![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png)
Each project is used to test the related project. Test projects use the following libraries for testing:
* [Xunit](https://xunit.github.io/) as the main test framework.
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as the assertion library.
* [NSubstitute](http://nsubstitute.github.io/) as the mocking library.
{{if DB=="EF"}}
> The test projects are configured to use **SQLite in-memory** as the database. A separate database instance is created and seeded (with the data seed system) to prepare a fresh database for every test.
{{else if DB=="Mongo"}}
> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)** library is used to mock the MongoDB database. A separate database instance is created and seeded (with the data seed system) to prepare a fresh database for every test.
{{end}}
## Adding Test Data
If you had created a data seed contributor as described in the [first part](Part-1.md), the same data will be available in your tests. So, you can skip this section. If you haven't created the seed contributor, you can use the `BookStoreTestDataSeedContributor` to seed the same data to be used in the tests below.
## Testing the BookAppService
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:
````csharp
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Xunit;
namespace Acme.BookStore.Books
{
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
}
}
````
* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of books.
* We can safely check the book "1984" by its name, because we know that this books is available in the database since we've added it in the seed data.
Add a new test method to the `BookAppService_Tests` class that creates a new **valid** book:
````csharp
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = System.DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
````
Add a new test that tries to create an invalid book and fails:
````csharp
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
````
* Since the `Name` is empty, ABP will throw an `AbpValidationException`.
The final test class should be as shown below:
````csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit;
namespace Acme.BookStore.Books
{
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = System.DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
}
}
````
Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests:
![bookstore-appservice-tests](./images/bookstore-appservice-tests.png)
Congratulations, the **green icons** indicates that the tests have been successfully passed!
## The Next Part
See the [next part](part-5.md) of this tutorial.

401
docs/en/Tutorials/Part-5.md

@ -0,0 +1,401 @@
# Web Application Development Tutorial - Part 5: Authorization
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"DB": ["EF","Mongo"]
}
````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## About This Tutorial
In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies:
* **{{DB_Text}}** as the ORM provider.
* **{{UI_Value}}** as the UI Framework.
This tutorial is organized as the following parts;
- [Part 1: Creating the project and book list page](Part-1.md)
- [Part 2: The book list page](Part-2.md)
- [Part 3: Creating, updating and deleting books](Part-3.md)
- [Part 4: Integration tests](Part-4.md)
- **Part 5: Authorization (this part)**
### Download the Source Code
This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded:
* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
## Permissions
ABP Framework provides an [authorization system](../Authorization.md) based on the ASP.NET Core's [authorization infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). One major feature added on top of the standard authorization infrastructure is the **permission system** which allows to define permissions and enable/disable per role, user or client.
### Permission Names
A permission must have a unique name (a `string`). The best way is to define it as a `const`, so we can reuse the permission name.
Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below:
````csharp
namespace Acme.BookStore.Permissions
{
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
}
````
This is a hierarchical way of defining permission names. For example, "create book" permission name was defined as `BookStore.Books.Create`.
### Permission Definitions
You should define permissions before using them.
Open the `BookStorePermissionDefinitionProvider` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below:
````csharp
using Acme.BookStore.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace Acme.BookStore.Permissions
{
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
}
````
This class defines a **permission group** (to group permissions on the UI, will be seen below) and **4 permissions** inside this group. Also, **Create**, **Edit** and **Delete** are children of the `BookStorePermissions.Books.Default` permission. A child permission can be selected **only if the parent was selected**.
Finally, edit the localization file (`en.json` under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project) to define the localization keys used above:
````json
"Permission:BookStore": "Book Store",
"Permission:Books": "Book Management",
"Permission:Books.Create": "Creating new books",
"Permission:Books.Edit": "Editing the books",
"Permission:Books.Delete": "Deleting the books"
````
> Localization key names are arbitrary and no forcing rule. But we prefer the convention used above.
### Permission Management UI
Once you define the permissions, you can see them on the **permission management modal**.
Go to the *Administration -> Identity -> Roles* page, select *Permissions* action for the admin role to open the permission management modal:
![bookstore-permissions-ui](images/bookstore-permissions-ui.png)
Grant the permissions you want and save the modal.
## Authorization
Now, you can use the permissions to authorize the book management.
### Application Layer & HTTP API
Open the `BookAppService` class and add set the policy names as the permission names defined above:
````csharp
using System;
using Acme.BookStore.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
{
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
}
}
````
Added code to the constructor. Base `CrudAppService` automatically uses these permissions on the CRUD operations. This makes the **application service** secure, but also makes the **HTTP API** secure since this service is automatically used as an HTTP API as explained before (see [auto API controllers](../API/Auto-API-Controllers.md)).
{{if UI == "MVC"}}
### Razor Page
While securing the HTTP API & the application service prevents unauthorized users to use the services, they can still navigate to the book management page. While they will get authorization exception when the page makes the first AJAX call to the server, we should also authorize the page for a better user experience and security.
Open the `BookStoreWebModule` and add the following code block inside the `ConfigureServices` method:
````csharp
Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default);
options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create);
options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit);
});
````
Now, unauthorized users are redirected to the **login page**.
#### Hide the New Book Button
The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission.
![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png)
Open the `Pages/Books/Index.cshtml` file and change the content as shown below:
````html
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Permissions
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@model IndexModel
@inject IStringLocalizer<BookStoreResource> L
@inject IAuthorizationService AuthorizationService
@section scripts
{
<abp-script src="/Pages/Books/Index.js"/>
}
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))
{
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
button-type="Primary"/>
}
</abp-column>
</abp-row>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable"></abp-table>
</abp-card-body>
</abp-card>
````
* Added `@inject IAuthorizationService AuthorizationService` to access to the authorization service.
* Used `@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))` to check the book creation permission to conditionally render the *New Book* button.
### JavaScript Side
Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
We should hide an action if the current user has not granted for the related permission. Datatables row actions has a `visible` option that can be set to `false` to hide the action item.
Open the `Pages/Books/Index.js` inside the `Acme.BookStore.Web` project and add a `visible` option to the `Edit` action as shown below:
````js
{
text: l('Edit'),
visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION
action: function (data) {
editModal.open({ id: data.record.id });
}
}
````
Do same for the `Delete` action:
````js
visible: abp.auth.isGranted('BookStore.Books.Delete')
````
* `abp.auth.isGranted(...)` is used to check a permission that is defined before.
* `visible` could also be get a function that returns a `bool` if the value will be calculated later, based on some conditions.
### Menu Item
Even we have secured all the layers of the book management page, it is still visible on the main menu of the application. We should hide the menu item if the current user has no permission.
Open the `BookStoreMenuContributor` class, find the code block below:
````csharp
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
)
)
);
````
And replace this code block with the following:
````csharp
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
));
}
````
{{else if UI == "NG"}}
### Angular Guard Configuration
First step of the UI is to prevent unauthorized users to see the "Books" menu item and enter to the book management page.
Open the `/src/app/book/book-routing.module.ts` and replace with the following content:
````js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';
import { BookComponent } from './book.component';
const routes: Routes = [
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class BookRoutingModule {}
````
* Imported `AuthGuard` and `PermissionGuard` from the `@abp/ng.core`.
* Added `canActivate: [AuthGuard, PermissionGuard]` to the route definition.
Open the `/src/app/route.provider.ts` and add `requiredPolicy: 'BookStore.Books'` to the `/books` route. The `/books` route block should be following:
````js
{
path: '/books',
name: '::Menu:Books',
parentName: '::Menu:BookStore',
layout: eLayoutType.application,
requiredPolicy: 'BookStore.Books',
}
````
### Hide the New Book Button
The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission.
![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png)
Open the `/src/app/book/book.component.html` file and replace the create button HTML content as shown below:
````html
<!-- Add the abpPermission directive -->
<button abpPermission="BookStore.Books.Create" id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ '::NewBook' | abpLocalization }}}%}</span>
</button>
````
* Just added `abpPermission="BookStore.Books.Create"` that hides the button if the current user has no permission.
### Hide the Edit and Delete Actions
Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
We should hide an action if the current user has not granted for the related permission.
Open the `/src/app/book/book.component.html` file and replace the edit and delete buttons contents as shown below:
````html
<!-- Add the abpPermission directive -->
<button abpPermission="BookStore.Books.Edit" ngbDropdownItem (click)="editBook(row.id)">
{%{{{ '::Edit' | abpLocalization }}}%}
</button>
<!-- Add the abpPermission directive -->
<button abpPermission="BookStore.Books.Delete" ngbDropdownItem (click)="delete(row.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
````
* Added `abpPermission="BookStore.Books.Edit"` that hides the edit action if the current user has no editing permission.
* Added `abpPermission="BookStore.Books.Delete"` that hides the delete action if the current user has no delete permission.
{{end}}

BIN
docs/en/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/en/Tutorials/images/bookstore-book-and-booktype.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Tutorials/images/bookstore-dbmigrator-on-solution.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/en/Tutorials/images/bookstore-edit-button-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
docs/en/Tutorials/images/bookstore-edit-delete-actions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/en/Tutorials/images/bookstore-empty-new-book-modal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/en/Tutorials/images/bookstore-getlist-result-network.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/en/Tutorials/images/bookstore-index-js-file-v3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
docs/en/Tutorials/images/bookstore-javascript-proxy-console.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/en/Tutorials/images/bookstore-new-book-button-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
docs/en/Tutorials/images/bookstore-new-book-button-small.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
docs/en/Tutorials/images/bookstore-permissions-ui.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

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

@ -201,77 +201,6 @@ this.config.dispatchGetAppConfiguration();
Note that **you do not have to call this method at application initiation**, because the application configuration is already being received from the server at start.
### How to Patch Route Configuration
The `dispatchPatchRouteByName` 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
const newRouteConfig: Partial<ABP.Route> = {
name: "Home",
path: "home",
children: [
{
name: "Dashboard",
path: "dashboard"
}
]
};
this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig);
// returns a state stream which emits after dispatch action is complete
```
### How to Add a New Route Configuration
The `dispatchAddRoute` adds a new route to the configuration state in the `Store`. For this, the route config should be passed as the parameter of the method.
```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"
};
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
import { eIdentityRouteNames } from '@abp/ng.identity';
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
parentName: eIdentityRouteNames.IdentityManagement,
name: "My New Page",
iconClass: "fa fa-dashboard",
path: "page",
invisible: false,
order: 2,
requiredPolicy: "MyProjectName.MyNewPage"
};
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'`.
#### Route Configuration Properties
Please refer to `ABP.Route` type for all the properties you can pass to `dispatchSetEnvironment` in its parameter. It can be found in the [common.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27).
### How to Set the Environment
The `dispatchSetEnvironment` places environment variables passed to it in the `Store` under the configuration state. Here is how it is used:

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

@ -1,39 +1,34 @@
# Custom Setting Page
There are several settings tabs from different modules. You can add custom settings page to your project in 3 steps.
There are several settings tabs from different modules. You can add a custom setting page to your project.
1. Create a Component
1. Create a component with the following command:
```js
import { Select } from '@ngxs/store';
import { Component } from '@angular/core';
@Component({
selector: 'app-your-custom-settings',
template: `
custom-settings works!
`,
})
export class YourCustomSettingsComponent {
// Your component logic
}
```bash
yarn ng generate component my-settings
```
2. Add the `YourCustomSettingsComponent` to `declarations` and the `entryComponents` arrays in the `AppModule`.
3. Open the `app.component.ts` and add the below content to the `ngOnInit`
2. Open the `app.component.ts` and modify the file as shown below:
```js
import { addSettingTab } from '@abp/ng.theme.shared';
// ...
ngOnInit() {
addSettingTab({
component: YourCustomSettingsComponent,
name: 'Type here the setting tab title (you can type a localization key, e.g: AbpAccount::Login',
order: 4,
requiredPolicy: 'type here a policy key'
});
import { Component } from '@angular/core';
import { SettingTabsService } from '@abp/ng.core'; // imported SettingTabsService
import { MySettingsComponent } from './my-settings/my-settings.component'; // imported MySettingsComponent
@Component(/* component metadata */)
export class AppComponent {
constructor(private settingTabs: SettingTabsService) // injected MySettingsComponent
{
// added below
settingTabs.add([
{
name: 'MySettings',
order: 1,
requiredPolicy: 'policy key here',
component: MySettingsComponent,
},
]);
}
}
```

114
docs/en/UI/Angular/List-Service.md

@ -53,59 +53,41 @@ class BookComponent {
}
```
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` properties directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component properties instead.
> Noticed `list` is `public` and `readonly`? That is because we will use `ListService` directly in the component's template. That may be considered as an anti-pattern, but it is much quicker to implement. You can always use public component members to expose the `ListService` instance instead.
Place `ListService` properties into the template like this:
Bind `ListService` to ngx-datatable like this:
```html
<abp-table
[value]="book.items"
[(page)]="list.page"
[rows]="list.maxResultCount"
[totalRecords]="book.totalCount"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[abpLoading]="list.isLoading$ | async"
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th (click)="nameSort.sort('name')">
{%{{{ '::Name' | abpLocalization }}}%}
<abp-sort-order-icon
#nameSort
sortKey="name"
[(selectedSortKey)]="list.sortKey"
[(order)]="list.sortOrder"
></abp-sort-order-icon>
</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
</tr>
</ng-template>
<!-- column templates here -->
</ngx-datatable>
```
## Usage with Observables
You may use observables in combination with [AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe) of Angular instead. Here are some possibilities:
```ts
```js
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query));
```
```html
<!-- simplified representation of the template -->
<abp-table
[value]="(book$ | async)?.items || []"
[totalRecords]="(book$ | async)?.totalCount"
<ngx-datatable
[rows]="(book$ | async)?.items || []"
[count]="(book$ | async)?.totalCount || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE -->
```
@ -113,7 +95,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
...or...
```ts
```js
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
@ -128,32 +110,41 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
```html
<!-- simplified representation of the template -->
<abp-table
[value]="books$ | async"
[totalRecords]="bookCount$ | async"
<ngx-datatable
[rows]="(books$ | async) || []"
[count]="(bookCount$ | async) || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
```
> We do not recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Refresh Table on Create/Update/Delete
`ListService` exposes a `get` method to trigger a request with the current query. So, basically, whenever a create, update, or delete action resolves, you can call `this.list.get();` and it will call hooked stream creator again.
```ts
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get);
```js
this.bookService.createByInput(form.value)
.subscribe(() => {
this.list.get();
// Other subscription logic here
});
```
...or...
```ts
this.bookService.createByInput(form.value)
.subscribe(() => {
this.list.get();
// Other subscription logic here
})
```js
this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get);
```
> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page.
## How to Implement Server-Side Search in a Table
`ListService` exposes a `filter` property that will trigger a request with the current query and the given search string. All you need to do is to bind it to an input element with two-way binding.
@ -163,3 +154,26 @@ this.bookService.createByInput(form.value)
<input type="text" name="search" [(ngModel)]="list.filter">
```
## Breaking Change with ABP v3.0
We had to modify the `ListService` to make it work with `ngx-datatable`. Previously, the minimum value for `page` property was `1` and you could use it like this:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[(page)]="list.page"
></abp-table>
```
As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for inital page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[page]="list.page + 1"
(pageChange)="list.page = $event - 1"
></abp-table>
```
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.

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

@ -146,16 +146,6 @@ Find [styles configuration in angular.json](https://angular.io/guide/workspace-c
"build": {
"options": {
"styles": [
{
"input": "node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css",
"inject": false,
"bundleName": "bootstrap-rtl.min"
},
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap-ltr.min"
},
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"inject": true,
@ -166,6 +156,16 @@ Find [styles configuration in angular.json](https://angular.io/guide/workspace-c
"inject": true,
"bundleName": "fontawesome-v4-shims.min"
},
{
"input": "node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css",
"inject": false,
"bundleName": "bootstrap-rtl.min"
},
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap-ltr.min"
},
"apps/dev-app/src/styles.scss"
],
}
@ -174,6 +174,7 @@ Find [styles configuration in angular.json](https://angular.io/guide/workspace-c
}
}
}
```
#### Step 2. Clear Lazy Loaded Fontawesome in AppComponent

477
docs/en/UI/Angular/Migration-Guide-v3.md

@ -0,0 +1,477 @@
# v2.9 to v3.0 Angular UI Migration Guide
## What Changed in v3.0?
### Angular 10
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though.
#### What to Do When Migrating?
Open a terminal at your root folder and run the following command:
```sh
yarn ng update @angular/cli @angular/core --force
```
This will make the following modifications:
- Update your package.json and install new packages
- Revise tsconfig.json files to create a "Solution Style" configuration
- Rename `browserlist` as `.browserlistrc`
On the other hand, it would be better if you check which packages to update first with `yarn ng update` command alone. Angular will give you a list of packages to update.
![Table of packages to update](./images/table-of-packages-to-update.png)
When Angular reports the packages above, your command would look like this:
```sh
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
```
> If Angular complains about uncommited changes in your repo, you can either commit/stash them or add `--allow-dirty` parameter to the command.
### Config Modules
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows:
```js
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
```
Although working, this had a few disadvantages:
- Every module came in two independent packages, but in reality, those packages were interdependent.
- Configuring lazy loaded modules required a wrapper module.
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size.
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...and in app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
```
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps.
#### A Better Example
AppModule:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
AppRoutingModule:
```js
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () =>
import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () =>
import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () =>
import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
AppComponent:
```js
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<abp-loader-bar></abp-loader-bar>
<abp-dynamic-layout></abp-dynamic-layout>
`,
})
export class AppComponent {}
```
> You may have noticed that we used `<abp-dynamic-layout>` instead of `<router-outlet>` in the AppComponent template. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your AppComponent.
#### What to Do When Migrating?
- Remove config packages from your project using `yarn remove`.
- Import config modules from secondary entry points (e.g. `@abp/ng.identity/config`).
- Call static `forRoot` method of all new config modules, even if a configuration is not passed.
- Call static `forRoot` method of `ThemeBasicModule` (or `ThemeLeptonModule` if commercial) and remove `SharedModule` from imports (unless you have added anything that is necessary for your root module in it).
- Import lazy ABP modules directly in app routing module (e.g. `() => import('@abp/ng.identity').then(...)`).
- Call static `forLazy` method of all lazy modules inside `then`, even if a configuration is not passed.
- [OPTIONAL] Add the `<abp-dynamic-layout></abp-dynamic-layout>` to the AppComponent template and remove the `<router-outlet></router-outlet>` for better performance and UX.
### RoutesService
In ABP v2.x, adding a route to the menu was done by one of two ways:
- [Via `routes` Property in `AppRoutingModule`](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule)
- [Via ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate)
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in `ConfigState` (breaking change). Instead, there is a new service called `RoutesService` which is used for adding, patching, or removing menu items. Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Check if you have ever used `ConfigState` or `ConfigStateService` to add any routes. If so, replace them with `add` method of `RoutesService`.
- Check if you have ever patched a route. If so, convert them to `patch` method of `RoutesService`.
- Double-check that you are using absolute paths and providing a `parentName` instead of `children` property for sub-menu items in `add` or `patch` method calls.
### NavItemsService
In ABP v2.x, adding a navigation element was done [via LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check [the documentation](./Modifying-the-Menu.md) for details.
#### What to Do When Migrating?
- Replace all `dispatchAddNavigationElement` calls with `addItems` method of `NavItemsService`.
### ngx-datatable
Until v3, we had been using a custom component, `abp-table`, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues.
As of ABP v3, we have switched to a battle-tested, well-executed data grid: [ngx-datatable](https://github.com/swimlane/ngx-datatable). All ABP modules will come with ngx-datatable already implemented in them. `ThemeSharedModule` already exports `NgxDatatableModule`. So, if you install the package by running `yarn add @swimlane/ngx-datatable` in your terminal, it will be available for use in all modules of your app.
For proper styling, you need to add the following in the styles section of your angular.json file (above all others):
```json
"styles": [
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
},
// other styles
]
```
Since `abp-table` is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the [ListService](./List-Service.md) to work well with ngx-datatable and [introduced](https://volosoft.com/blog/attribute-directives-to-avoid-repetition-in-angular-templates) two new directives: `NgxDatatableListDirective` and `NgxDatatableDefaultDirective`.
The usage of those directives is rather simple:
```js
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
```
...and in component template...
```html
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
```
Once you bind the injected `ListService` instance through `NgxDatatableListDirective`, you no longer need to worry about pagination or sorting. Similarly, `NgxDatatableDefaultDirective` gets rid of several property bindings to make ngx-datatable fit our styles.
#### A Better Example
```html
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
```
#### What to Do When Migrating?
- Install `@swimlane/ngx-datatable` package.
- Add ngx-datatable styles in the angular.json file.
- If you can, update your modules according to the example above.
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the [breaking change described here](./List-Service.md).
**Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.
### Extensions System [COMMERCIAL]
The extensions system is open sourced now and is publicly available from `@abp/ng.theme.shared/extensions` package instead of `@volo/abp.commercial.ng.ui`. Also, according to the new structure of config packages, the configuration is given through `forLazy` static methods as described above.
#### What to Do When Migrating?
If you have never used the extensions system before, you do not have to do anything. If you have, then please check the documentation again to see what changed. Extension system itself works the same as before. The only changes are the package you import from and the static method and the module you pass your contributors to.
### Lepton Theme Logos [COMMERCIAL]
In ABP v2.x, Lepton had one light and one dark logo per color theme. We have realized we could make it work with only one light and one dark logo. So, we have changed how Lepton looks up logo images and now you just need to have a `logo-light.png` and a `logo-dark.png` in your project.
#### What to Do When Migrating?
If you have switched template logo PNGs before, the change is simple:
- Go to `/assets/images/logo` folder.
- Rename `theme1.png` as `logo-light.png` and `theme1-reverse.png` as `logo-dark.png`.
- Delete all other `theme*.png` files.
If you have replaced the logo component(s), the change is a little bit different, but still simple. The `LayoutStateService` has a two new members: `primaryLogoColor` and `secondaryLogoColor`. They have an observable stream of `'light'` and `'dark'` strings as value. You can consume their value in your custom logo component templates with the `async` pipe. Here is a complete example which covers both primary and secondary (account) layout logos.
```js
import { AddReplaceableComponent } from '@abp/ng.core';
import { CommonModule } from '@angular/common';
import { APP_INITIALIZER, Component, Injector, NgModule } from '@angular/core';
import { Store } from '@ngxs/store';
import { eAccountComponents } from '@volo/abp.ng.account';
import {
AccountLayoutComponent,
eThemeLeptonComponents,
LayoutStateService,
} from '@volo/abp.ng.theme.lepton';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
template: `
<div class="account-brand p-4 text-center mb-1" *ngIf="isAccount; else link">
<ng-template [ngTemplateOutlet]="link"></ng-template>
</div>
<ng-template #link>
<a [style.background-image]="logoUrl | async" class="navbar-brand" routerLink="/"></a>
</ng-template>
`,
})
export class LogoComponent {
isAccount: boolean;
logoColor: Observable<'dark' | 'light'>;
get logoUrl() {
return this.logoColor.pipe(map(color => `url(/assets/images/logo/logo-${color}.png)`));
}
constructor(injector: Injector) {
const layout = injector.get(LayoutStateService);
this.isAccount = Boolean(injector.get(AccountLayoutComponent, false));
this.logoColor = this.isAccount ? layout.secondaryLogoColor : layout.primaryLogoColor;
}
}
@NgModule({
imports: [CommonModule],
declarations: [LogoComponent],
exports: [LogoComponent],
})
export class LogoModule {}
export const APP_LOGO_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: switchLogos, multi: true, deps: [Store] },
];
export function switchLogos(store: Store) {
return () => {
store.dispatch(
new AddReplaceableComponent({
component: LogoComponent,
key: eThemeLeptonComponents.Logo,
}),
);
store.dispatch(
new AddReplaceableComponent({
component: LogoComponent,
key: eAccountComponents.Logo,
}),
);
};
}
```
Just add `APP_LOGO_PROVIDER` to the providers of your root module (usually `AppModule`) and you will have a custom logo component adjusting to the theme colors.
### Deprecated Interfaces
Some interfaces have long been marked as deprecated and now they are removed.
#### What to Do When Migrating?
- Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281)
## What's Next?
* [Service Proxies](./Service-Proxies.md)

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

@ -3,9 +3,6 @@
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.
@ -25,9 +22,99 @@ export const environment = {
## How to Add a Navigation Element
### Via `RoutesService`
You can add routes to the menu by calling the `add` method of `RoutesService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
]);
}
}
```
An alternative and probably cleaner way is to use a route provider. First create a provider:
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
requiredPolicy: 'permission key here',
order: 101,
iconClass: 'fas fa-question-circle',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
requiredPolicy: 'permission key here',
order: 1,
},
]);
};
}
```
...and then in app.module.ts...
```js
import { NgModule } from '@angular/core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
// imports, declarations, and bootstrap
})
export class AppModule {}
```
Here is what every property works as:
- `path` is the absolute path of the navigation element.
- `name` is the label of the navigation element. A localization key or a localization object can be passed.
- `parentName` is a reference to the `name` of the parent route in the menu and is used for creating multi-level menu items.
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `order` is the order of the navigation element. "Administration" has an order of `100`, so keep that in mind when ordering top level menu items.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `layout` defines in which layout the route will be loaded. (default: `eLayoutType.empty`)
- `invisible` makes the item invisible in the menu. (default: `false`)
### 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 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 `RoutesService`.
You can add the `routes` property like below:
@ -37,7 +124,7 @@ You can add the `routes` property like below:
data: {
routes: {
name: 'Your navigation',
order: 3,
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
children: [
@ -48,150 +135,142 @@ You can add the `routes` property like below:
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:**
Alternatively, 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
{
path: 'your-path',
data: {
routes: [
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
] as ABP.Route[], // can be imported from @abp/ng.core
},
}
```
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 advantage of the second method is that you are not bound to the parent/child structure and use any paths you like.
The new route will be added like below:
After adding the `routes` property as described above, the navigation menu looks like this:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png)
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## How to Patch a Navigation Element
## How to Patch or Remove 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.
The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children.
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
// this.routes is instance of RoutesService
// eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
const dashboardRouteConfig: ABP.Route = {
path: '/dashboard',
name: '::Menu:Dashboard',
parentName: '::Menu:Home',
order: 1,
layout: eLayoutType.application,
};
const newRouteConfig: Partial<ABP.Route> = {
const newHomeRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration,
parentName: eThemeSharedRouteNames.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
this.routes.add([dashboardRouteConfig]);
this.routes.patch('::Menu:Home', newHomeRouteConfig);
this.routes.remove(['Your navigation']);
```
* Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
* Added an icon.
* Specified the order.
* Added a child route named _Dashboard_.
- Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`.
- Added an icon to _Home_.
- Specified the order and made _Home_ the first item in list.
- Added a route named _Dashboard_ as a child of _Home_.
- Removed _Your navigation_ along with its child route.
After the patch above, navigation elements looks like below:
After the operations above, the new menu 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:
You can add elements to the right part of the menu by calling the `addItems` method of `NavItemsService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
```js
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line
import { NavItemsService } from '@abp/ng.theme.shared';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<!-- Added below content -->
<ng-template #search
><input type="search" placeholder="Search" class="bg-transparent border-0"
/></ng-template>
<input type="search" placeholder="Search" class="bg-transparent border-0 color-white" />
`,
})
export class AppComponent {
// Added ViewChild
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>;
export class MySearchInputComponent {}
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);
@Component(/* component metadata */)
export class AppComponent {
constructor(private navItems: NavItemsService) {
navItems.addItems([
{
id: 'MySearchInput',
order: 1,
component: MySearchInputComponent,
},
{
id: 'SignOutIcon',
html: '<i class="fas fa-sign-out-alt fa-lg text-white m-2"><i>',
action: () => console.log('Clicked the sign out icon'),
order: 101, // puts as last element
},
]);
}
}
```
This inserts a search input to the menu. The final UI looks like below:
This inserts a search input and a sign out icon 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
> The default elements have an order of `100`. If you want to place a custom element before the defaults, assign an order number up to `99`. If you want to place a custom element after the defaults, assign orders starting from `101`. Finally, if you must place an item between the defaults, patch the default element orders as described below. A warning though: We may add another default element in the future and it too will have an order number of `100`.
## How to Patch or Remove an Right Part Element
The `patchItem` method of `NavItemsService` finds an element by its `id` property and replaces its configuration with the new configuration passed as the second parameter. Similarly, `removeItem` method finds an element and removes it.
```js
export class AppComponent {
constructor(private navItems: NavItemsService) {
navItems.patchItem(eThemeBasicComponents.Languages, {
requiredPolicy: 'new policy here',
order: 1,
});
navItems.removeItem(eThemeBasicComponents.CurrentUser);
}
}
```
TODO
* Patched the languages dropdown element with new `requiredPolicy` and new `order`.
* Removed the current user dropdown element.
## What's Next

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

@ -8,7 +8,7 @@ You can get permission as boolean value from store:
```js
import { Store } from '@ngxs/store';
import { ConfigState } from '../states';
import { ConfigState } from '@abp/ng.core';
export class YourComponent {
constructor(private store: Store) {}
@ -24,7 +24,7 @@ export class YourComponent {
Or you can get it via `ConfigStateService`:
```js
import { ConfigStateService } from '../services/config-state.service';
import { ConfigStateService } from '@abp/ng.core';
export class YourComponent {
constructor(private configStateService: ConfigStateService) {}
@ -42,7 +42,7 @@ export class YourComponent {
You can use the `PermissionDirective` to manage visibility of a DOM Element accordingly to user's permission.
```html
<div *abpPermission="AbpIdentity.Roles">
<div *abpPermission="'AbpIdentity.Roles'">
This content is only visible if the user has 'AbpIdentity.Roles' permission.
</div>
```
@ -55,18 +55,20 @@ The directive can also be used as an attribute directive but we recommend to you
You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation.
Add `requiredPolicy` to the `routes` property in your routing module.
* Import the PermissionGuard from @abp/ng.core.
* Add `canActivate: [PermissionGuard]` to your route object.
* Add `requiredPolicy` to the `data` property of your route in your routing module.
```js
import { PermissionGuard } from '@abp/ng.core';
// ...
const routes: Routes = [
{
path: 'path',
component: YourComponent,
canActivate: [PermissionGuard],
data: {
routes: {
requiredPolicy: 'AbpIdentity.Roles.Create',
},
requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component
},
},
];

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

@ -32,7 +32,7 @@ A variable named `apiName` (available as of v2.4) is defined in each service. `a
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';
import { AbpApplicationConfigurationService } from '../abp/applicationconfiguration/services';
//...
export class HomeComponent{
@ -48,14 +48,14 @@ The Angular compiler removes the services that have not been injected anywhere f
### Models
The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/shared/models` folder.
The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/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';
import { IdentityRoleCreateDto } from '../identity/role/models'
//...
const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true})
```

BIN
docs/en/UI/Angular/images/custom-settings.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 106 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 63 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/en/UI/Angular/images/table-of-packages-to-update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

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

@ -11,92 +11,95 @@ Basic usage:
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
{
[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 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 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 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
}
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.
@ -125,7 +128,7 @@ 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.
By default, `abp-dynamic-form` 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:
@ -152,23 +155,23 @@ 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; }
}
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`.
@ -198,55 +201,57 @@ If you have read the [Form elements document](Form-elements.md), you noticed tha
````xml
<abp-dynamic-form abp-model="@Model.MyDetailedModel"/>
````
Model:
````csharp
public class DynamicFormsModel : PageModel
{
[BindProperty]
public DetailedModel MyDetailedModel { get; set; }
{
[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 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 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 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
}
public enum CarType
{
Sedan,
Hatchback,
StationWagon,
Coupe
}
}
````
As you see in example above:
@ -265,13 +270,6 @@ By default, it will try to find "DisplayName:{PropertyName}" or "{PropertyName}"
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; }
````
[Display(Name = "Name")]
public string Name { get; set; }
````

357
docs/en/Unit-Of-Work.md

@ -1,3 +1,356 @@
## Unit of Work
# Unit of Work
ABP Framework's Unit Of Work (UOW) implementation provides an abstraction and control on a **database connection and transaction** scope in an application.
Once a new UOW started, it creates an **ambient scope** that is participated by **all the database operations** performed in the current scope and considered as a **single transaction boundary**. The operations are **committed** (on success) or **rolled back** (on exception) all together.
ABP's UOW system is;
* **Works conventional**, so most of the times you don't deal with UOW at all.
* **Database provider independent**.
* **Web independent**, that means you can create unit of work scopes in any type of applications beside web applications/services.
## Conventions
The following method types are considered as a unit of work:
* ASP.NET Core MVC **Controller Actions**.
* ASP.NET Core Razor **Page Handlers**.
* **Application service** methods.
* **Repository methods**.
A UOW automatically begins for these methods **except** if there is already a **surrounding (ambient)** UOW in action. Examples;
* If you call a [repository](Repositories.md) method and there is no UOW started yet, it automatically **begins a new transactional UOW** that involves all the operations done in the repository method and **commits the transaction** if the repository method **doesn't throw any exception.** The repository method doesn't know about UOW or transaction at all. It just works on a regular database objects (`DbContext` for [EF Core](Entity-Framework-Core.md), for example) and the UOW is handled by the ABP Framework.
* If you call an [application service](Application-Services.md) method, the same UOW system works just as explained above. If the application service method uses some repositories, the repositories **don't begin a new UOW**, but **participates to the current unit of work** started by the ABP Framework for the application service method.
* The same is true for an ASP.NET Core controller action. If the operation has started with a controller action, then the **UOW scope is the controller action's method body**.
All of these are automatically handled by the ABP Framework.
### Database Transaction Behavior
While the section above explains the UOW as it is database transaction, actually a UOW doesn't have to be transactional. By default;
* **HTTP GET** requests don't start a transactional UOW. They still starts a UOW, but **doesn't create a database transaction**.
* All other HTTP request types start a UOW with a database transaction, if database level transactions are supported by the underlying database provider.
This is because an HTTP GET request doesn't (and shouldn't) make any change in the database. You can change this behavior using the options explained below.
## Default Options
`AbpUnitOfWorkDefaultOptions` is used to configure the default options for the unit of work system. Configure the options in the `ConfigureServices` method of your [module](Module-Development-Basics.md).
**Example: Completely disable the database transactions**
````csharp
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});
````
### Option Properties
* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). A global point to configure the transaction behavior. Default value is `Auto` and work as explained in the "*Database Transaction Behavior*" section above. You can enable (even for HTTP GET requests) or disable transactions with this option.
* `TimeOut` (`int?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider.
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional.
## Controlling the Unit Of Work
In some cases, you may want to change the conventional transaction scope, create inner scopes or fine control the transaction behavior. The following sections cover these possibilities.
### IUnitOfWorkEnabled Interface
This is an easy way to enable UOW for a class (or a hierarchy of classes) that is not unit of work by the conventions explained above.
**Example: Implement `IUnitOfWorkEnabled` for an arbitrary service**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency, IUnitOfWorkEnabled
{
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
}
}
````
Then `MyService` (and any class derived from it) methods will be UOW.
However, there are **some rules should be followed** in order to make it working;
* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work).
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted. So, sync methods can not start a UOW.
> Notice that if `FooAsync` is called inside a UOW scope, then it already participates to the UOW without needing to the `IUnitOfWorkEnabled` or any other configuration.
### UnitOfWorkAttribute
`UnitOfWork` attribute provides much more possibility like enabling or disabling UOW and controlling the transaction behavior.
`UnitOfWork` attribute can be used for a **class** or a **method** level.
**Example: Enable UOW for a specific method of a class**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
[UnitOfWork]
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
public virtual async Task BarAsync()
{
//this is a method without UOW
}
}
}
````
**Example: Enable UOW for all the methods of a class**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
[UnitOfWork]
public class MyService : ITransientDependency
{
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
public virtual async Task BarAsync()
{
//this is a method with a UOW scope
}
}
}
````
Again, the **same rules** are valid here:
* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work).
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted. So, sync methods can not start a UOW.
#### UnitOfWorkAttribute Properties
* `IsTransactional` (`bool?`): Used to set whether the UOW should be transactional or not. **Default value is `null`**. if you leave it `null`, it is determined automatically based on the conventions and the configuration.
* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value.
* `IsDisabled` (`bool`): Used to disable the UOW for the current method/class.
> If a method is called in an ambient UOW scope, then the `UnitOfWork` attribute is ignored and the method participates to the surrounding transaction in any way.
**Example: Disable UOW for a controller action**
````csharp
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Uow;
namespace AbpDemo.Web
{
public class MyController : AbpController
{
[UnitOfWork(IsDisabled = true)]
public virtual async Task FooAsync()
{
//...
}
}
}
````
## IUnitOfWorkManager
`IUnitOfWorkManager` is the main service that is used to control the unit of work system. The following sections explains how to directly work with this service (while most of the times you won't need).
### Begin a New Unit Of Work
`IUnitOfWorkManager.Begin` method is used to create a new UOW scope.
**Example: Create a new non-transactional UOW scope**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public virtual async Task FooAsync()
{
using (var uow = _unitOfWorkManager.Begin(
requiresNew: true, isTransactional: false
))
{
//...
await uow.CompleteAsync();
}
}
}
}
````
`Begin` method gets the following optional parameters:
* `requiresNew` (`bool`): Set `true` to ignore the surrounding unit of work and start a new UOW with the provided options. **Default value is `false`. If it is `false` and there is a surrounding UOW, `Begin` method doesn't actually begin a new UOW, but silently participates to the existing UOW.**
* `isTransactional` (`bool`). Default value is `false`.
* `isolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value.
* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
### The Current Unit Of Work
UOW is ambient, as explained before. If you need to access to the current unit of work, you can use the `IUnitOfWorkManager.Current` property.
**Example: Get the current UOW**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyProductService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyProductService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public async Task FooAsync()
{
var uow = _unitOfWorkManager.Current;
//...
}
}
}
````
`Current` property returns a `IUnitOfWork` object.
> **Current Unit Of Work can be `null`** if there is no surrounding unit of work. It won't be `null` if your class is a conventional UOW class, you manually made it UOW or it was called inside a UOW scope, as explained before.
#### SaveChangesAsync
`IUnitOfWork.SaveChangesAsync()` method can be needed to save all the changes until now to the database. If you are using EF Core, it behaves exactly same. If the current UOW is transactional, even saved changes can be rolled back on an error (for the supporting database providers).
**Example: Save changes after inserting an entity to get its auto-increment id**
````csharp
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace AbpDemo
{
public class CategoryAppService : ApplicationService, ICategoryAppService
{
private readonly IRepository<Category, int> _categoryRepository;
public CategoryAppService(IRepository<Category, int> categoryRepository)
{
_categoryRepository = categoryRepository;
}
public async Task<int> CreateAsync(string name)
{
var category = new Category {Name = name};
await _categoryRepository.InsertAsync(category);
//Saving changes to be able to get the auto increment id
await UnitOfWorkManager.Current.SaveChangesAsync();
return category.Id;
}
}
}
````
This example uses auto-increment `int` primary key for the `Category` [entity](Entities.md). Auto-increment PKs require to save the entity to the database to get the id of the new entity.
This example is an [application service](Application-Services.md) derived from the base `ApplicationService` class, which already has the `IUnitOfWorkManager` service injected as the `UnitOfWorkManager` property. So, no need to inject it manually.
Since getting the current UOW is pretty common, there is also a `CurrentUnitOfWork` property as a shortcut to the `UnitOfWorkManager.Current`. So, the example above can be changed to use it:
````csharp
await CurrentUnitOfWork.SaveChangesAsync();
````
##### Alternative to the SaveChanges()
Since saving changes after inserting, updating or deleting an entity can be frequently needed, corresponding [repository](Repositories.md) methods has an optional `autoSave` parameter. So, the `CreateAsync` method above could be re-written as shown below:
````csharp
public async Task<int> CreateAsync(string name)
{
var category = new Category {Name = name};
await _categoryRepository.InsertAsync(category, autoSave: true);
return category.Id;
}
````
If your intent is just to save the changes after creating/updating/deleting an entity, it is suggested to use the `autoSave` option instead of manually using the `CurrentUnitOfWork.SaveChangesAsync()`.
> **Note-1**: All changes are automatically saved when a unit of work ends without any error. So, don't call `SaveChangesAsync()` and don't set `autoSave` to `true` unless you really need it.
>
> **Note-2**: If you use `Guid` as the primary key, you never need to save changes on insert to just get the generated id, because `Guid` keys are set in the application and are immediately available once you create a new entity.
#### Other IUnitOfWork Properties/Methods
* `OnCompleted` method gets a callback action which is called when the unit of work successfully completed (where you can be sure that all changes are saved).
* `Failed` and `Disposed` events can be used to be notified if the UOW fails or when it is disposed.
* `Complete` and `Rollback` methods are used to complete (commit) or roll backs the current UOW, which are normally used internally by the ABP Framework but can be used if you manually start a transaction using the `IUnitOfWorkManager.Begin` method.
* `Options` can be used to get options that was used while starting the UOW.
* `Items` dictionary can be used to store and get arbitrary objects inside the same unit of work, which can be a point to implement custom logics.
## ASP.NET Core Integration
Unit of work system is fully integrated to the ASP.NET Core. It properly works when you use ASP.NET Core MVC Controllers or Razor Pages. It defines action filters and page filters for the UOW system.
> You typically do nothing to configure the UOW when you use ASP.NET Core.
### Unit Of Work Middleware
`AbpUnitOfWorkMiddleware` is a middleware that can enable UOW in the ASP.NET Core request pipeline. This might be needed if you need to enlarge the UOW scope to cover some other middleware(s).
**Example:**
````csharp
app.UseUnitOfWork();
app.UseConfiguredEndpoints();
````
TODO

180
docs/en/Virtual-File-System.md

@ -1,40 +1,30 @@
## Virtual File System
# Virtual File System
The Virtual File System makes it possible to manage files that do not physically exist on the file system (disk). It's mainly used to embed (js, css, image..) files into assemblies and use them like physical files at runtime.
### Volo.Abp.VirtualFileSystem Package
## Installation
Volo.Abp.VirtualFileSystem is the core package of the virtual file system. Install it in your project using the package manager console (PMC):
> Most of the times you don't need to manually install this package since it comes pre-installed with the [application startup template](Startup-Templates/Application.md).
```
Install-Package Volo.Abp.VirtualFileSystem
```
[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) is the main page of the Virtual File System.
> This package is already installed by default with the startup template. So, most of the time, you will not need to install it manually.
Use the ABP CLI to add this package to your project:
Then you can add **AbpVirtualFileSystemModule** dependency to your module:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.VirtualFileSystem` package.
* Run `abp add-package Volo.Abp.VirtualFileSystem` command.
```c#
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
If you want to do it manually, install the [Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpVirtualFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
//...
}
}
```
## Working with the Embedded Files
#### Registering Embedded Files
### Embed the Files
A file should be first marked as an embedded resource to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example:
A file should be first marked as an **embedded resource** to embed the file into the assembly. The easiest way to do it is to select the file from the **Solution Explorer** and set **Build Action** to **Embedded Resource** from the **Properties** window. Example:
![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png)
If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your **.csproj** file:
If you want to add multiple files, this can be tedious. Alternatively, you can directly edit your `.csproj` file:
````C#
<ItemGroup>
@ -45,76 +35,60 @@ If you want to add multiple files, this can be tedious. Alternatively, you can d
This configuration recursively adds all files under the **MyResources** folder of the project (including the files you will add in the future).
Then the module needs to be configured using `AbpVirtualFileSystemOptions` to register the embedded files to the virtual file system. Example:
Embedding a file in the project/assembly may cause problems if a file name contains some special chars. To overcome this limitation;
````C#
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
1. Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project that contains the embedded resource(s).
2. Add `<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>` into the `<PropertyConfig>...</PropertyConfig>` section of your `.csproj` file.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//Register all embedded files of this assembly to the virtual file system
options.FileSets.AddEmbedded<MyModule>("YourRootNameSpace");
});
> While these two steps are optional and ABP can work without these configuration, it is strongly suggested to make it.
//...
}
}
}
````
### Configure the AbpVirtualFileSystemOptions
The `AddEmbedded` extension method takes a class, finds all embedded files from the assembly of the given class and registers them to the virtual file system. More concisely it could be written as follows:
Use `AbpVirtualFileSystemOptions` [options class](Options.md) to register the embedded files to the virtual file system in the `ConfigureServices` method of your [module](Module-Development-Basics.md).
````C#
options.FileSets.Add(
new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace");
**Example: Add embedded files to the virtual file system**
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
````
> "YourRootNameSpace" is the root namespace of your project. It can be empty if your root namespace is empty.
The `AddEmbedded` extension method takes a class, finds all embedded files from the **assembly of the given class** and registers them to the virtual file system. It is common to pass the module class as the generic argument.
#### Getting Virtual Files: IVirtualFileProvider
`AddEmbedded` can get two optional parameters;
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents:
* `baseNamespace`: This may only needed if you didn't configure the `GenerateEmbeddedFilesManifest` step explained above and your root namespace is not empty. In this case, set your root namespace here.
* `baseFolder`: If you don't want to expose all embedded files in the project, but only want to expose a specific folder (and sub folders/files), then you can set the base folder relative to your project root page.
````C#
public class MyService
**Example: Add files under the `MyFiles` folder in the project**
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
private readonly IVirtualFileProvider _virtualFileProvider;
options.FileSets.AddEmbedded<MyModule>(
baseNamespace: "Acme.BookStore.MyFiles",
baseFolder: "/MyFiles"
);
});
````
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
This example assumes;
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP
* Your project root (default) namespace is `Acme.BookStore`.
* Your project has a folder, named `MyFiles`
* You only want to add `MyFiles` folder to the virtual file system.
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js");
}
}
````
### Dealing With Embedded Files During Development
#### Dealing With Embedded Files During Development
Embedding a file into a module assembly and being able to use it from another project just by referencing the assembly (or adding a nuget package) is invaluable for creating a re-usable module. However, it does make it a little bit harder to develop the module itself.
Embedding a file into an assembly and being able to use it from another project just by referencing the assembly (or adding a NuGet package) is invaluable for creating a re-usable module. However, it makes it a little bit harder to develop the module itself.
Let's assume that you're developing a module that contains an embedded JavaScript file. Whenever you change this file you must re-compile the project, re-start the application and refresh the browser page to take the change. Obviously, this is very time consuming and tedious.
What is needed is the ability for the application to directly use the physical file at development time and a have a browser refresh reflect any change made in the JavaScript file. The `ReplaceEmbeddedByPhysical` method makes all this possible.
The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time.
The example below shows an application that depends on a module (`MyModule`) that itself contains embedded files. The application can reach the source code of the module at development time.
````C#
[DependsOn(typeof(MyModule))]
@ -128,29 +102,63 @@ public class MyWebAppModule : AbpModule
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//ReplaceEmbeddedByPhysical gets the root folder of the MyModule project
options.FileSets.ReplaceEmbeddedByPhysical<MyModule>(
Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}MyModuleProject", Path.DirectorySeparatorChar))
Path.Combine(
hostingEnvironment.ContentRootPath,
string.Format(
"..{0}MyModuleProject",
Path.DirectorySeparatorChar
)
)
);
});
}
//...
}
}
````
The code above assumes that `MyWebAppModule` and `MyModule` are two different projects in a Visual Studio solution and `MyWebAppModule` depends on the `MyModule`.
### ASP.NET Core Integration
> The [application startup template](Startup-Templates/Application.md) already uses this technique for the localization files. So, when you change a localization file it automatically detects the change.
## IVirtualFileProvider
After embedding a file into an assembly and registering it to the virtual file system, the `IVirtualFileProvider` interface can be used to get files or directory contents:
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider
.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString();
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider
.GetDirectoryContents("/MyResources/js");
}
}
````
## ASP.NET Core Integration
The Virtual File System is well integrated to ASP.NET Core:
* Virtual files can be used just like physical (static) files in a web application.
* Js, css, image files and all other web content types can be embedded into assemblies and used just like the physical files.
* An application (or another module) can override a virtual file of a module just like placing a file with the same name and extension into the same folder of the virtual file.
* An application (or another module) can **override a virtual file** of a module just like placing a file with the same name and extension into the same folder of the virtual file.
#### Virtual Files Middleware
### UseVirtualFiles Middleware
The Virtual Files Middleware is used to serve embedded (js, css, image...) files to clients/browsers just like physical files in the **wwwroot** folder. Add it just after the static file middleware as shown below:
@ -160,4 +168,14 @@ app.UseVirtualFiles();
Adding virtual files middleware after the static files middleware makes it possible to override a virtual file with a real physical file simply by placing it in the same location as the virtual file.
>The Virtual File Middleware only serves the virtual wwwroot folder contents - just like the other static files.
> `UseVirtualFiles()` is already configured for the [application startup template](Startup-Templates/Application.md).
#### Static Virtual File Folders
By default, ASP.NET Core only allows the `wwwroot` folder to contain the static files consumed by the clients. When you use the `UseVirtualFiles` middleware, the following folders also can contain static files:
* Pages
* Views
* Themes
This allows to add `.js`, `.css`... files near to your `.cshtml` file that is easier to develop and maintain your project.

45
docs/en/docs-nav.json

@ -26,19 +26,27 @@
"text": "Tutorials",
"items": [
{
"text": "Application Development",
"text": "Web Application Development",
"items": [
{
"text": "Part-1: Creating a new solution and listing items",
"text": "1: Creating the Server Side",
"path": "Tutorials/Part-1.md"
},
{
"text": "Part-2: CRUD operations",
"text": "2: The Book List Page",
"path": "Tutorials/Part-2.md"
},
{
"text": "Part-3: Integration tests",
"text": "3: Creating, Updating and Deleting Books",
"path": "Tutorials/Part-3.md"
},
{
"text": "4: Integration Tests",
"path": "Tutorials/Part-4.md"
},
{
"text": "5: Authorization",
"path": "Tutorials/Part-5.md"
}
]
}
@ -153,16 +161,23 @@
]
},
{
"text": "Events",
"text": "Event Bus",
"items": [
{
"text": "Event Bus (local)"
"text": "Overall",
"path": "Event-Bus.md"
},
{
"text": "Local Event Bus",
"path": "Local-Event-Bus.md"
},
{
"text": "Distributed Event Bus",
"path": "Distributed-Event-Bus.md",
"items": [
{
"text": "RabbitMQ Integration"
"text": "RabbitMQ Integration",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
}
]
}
@ -285,7 +300,8 @@
"path": "Data-Transfer-Objects.md"
},
{
"text": "Unit Of Work"
"text": "Unit Of Work",
"path": "Unit-Of-Work.md"
}
]
}
@ -358,6 +374,10 @@
{
"text": "Angular",
"items": [
{
"text": "Migration Guide v2.x to v3",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{
"text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md"
@ -420,6 +440,15 @@
}
]
},
{
"text": "React Native",
"items": [
{
"text": "Getting Started",
"path": "Getting-Started-React-Native.md"
}
]
},
{
"text": "Common",
"items": [

8
docs/pt-BR/Tutorials/Angular/Part-II.md

@ -195,7 +195,7 @@ Abra `book-list.component.html`e adicione o formulário no modelo de corpo do mo
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {%{{{ type }}}%}</option>
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option>
</select>
</div>
@ -218,18 +218,18 @@ Abra `book-list.component.html`e adicione o formulário no modelo de corpo do mo
> Usamos o [datepicker do NgBootstrap](https://ng-bootstrap.github.io/#/components/datepicker/overview) neste componente.
Abra o `book-list.component.ts`e crie uma matriz chamada `bookTypeArr`:
Abra o `book-list.component.ts`e crie uma matriz chamada `bookTypes`:
```js
//...
form: FormGroup;
bookTypeArr = Object.keys(Books.BookType).filter(
bookTypes = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
```
O `bookTypeArr`contém os campos da `BookType`enumeração. A matriz resultante é mostrada abaixo:
O `bookTypes`contém os campos da `BookType`enumeração. A matriz resultante é mostrada abaixo:
```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]

2
docs/zh-Hans/Blob-Storing.md

@ -169,7 +169,7 @@ public class ProfileAppService : ApplicationService
如果不使用泛型参数,直接注入 `IBlobContainer` (如上所述),会得到默认容器. 注入默认容器的另一种方法是使用 `IBlobContainer<DefaultContainer>`,它返回完全相同的容器.
默认容器的名称是 `Default`.
默认容器的名称是 `default`.
### 命令容器

3
docs/zh-Hans/CLI.md

@ -169,7 +169,8 @@ abp update [options]
* `--nuget`: 仅更新的NuGet包
* `--solution-path``-sp`: 指定解决方案路径/目录. 默认使用当前目录
* `--solution-name``-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件.
*`--check-all`: 分别检查每个包的新版本. 默认是 `false`.
### 切换到每晚构建(预览)包
想要切换到ABP框架的最新预览版可以使用此命令.

134
docs/zh-Hans/Distributed-Event-Bus-RabbitMQ-Integration.md

@ -0,0 +1,134 @@
# 分布式事件总线RabbitMQ集成
> 本文解释了**如何配置[RabbitMQ](https://www.rabbitmq.com/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.RabbitMQ[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.RabbitMQ` 包的 `.csproj` 文件目录打开命令行(终端).
* 运行 `abp add-package Volo.Abp.EventBus.RabbitMQ` 命令.
如果你想要手动安装,安装[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusRabbitMqModule))]` 到你的项目[模块](Module-Development-Basics.md)类.
## 配置
可以使用配置使用标准的[配置系统](Configuration.md),如 `appsettings.json` 文件,或[选项](Options.md)类.
### `appsettings.json` 文件配置
这是配置RabbitMQ设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持的](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
**示例:最小化配置与默认配置连接到本地的RabbitMQ服务器**
````json
{
"RabbitMQ": {
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName"
}
}
}
````
* `ClientName` 是应用程序的名称,用于RabbitMQ的**队列名称**.
* `ExchangeName`**交换机名称**.
参阅[RabbitMQ文档](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues)更好的了解这些选项.
#### 连接
如果需要连接到本地主机以外的另一台服务器,需要配置连接属性.
**示例: 指定主机名 (如IP地址)**
````json
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123"
}
},
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName"
}
}
}
````
允许定义多个连接. 在这种情况下,你可以指定用于事件总线的连接.
**示例: 声明两个连接并将其中一个用于事件总线**
````json
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123"
},
"SecondConnection": {
"HostName": "321.321.321.321"
}
},
"EventBus": {
"ClientName": "MyClientName",
"ExchangeName": "MyExchangeName",
"ConnectionName": "SecondConnection"
}
}
}
````
这允许你可以在你的应用程序使用多个RabbitMQ服务器,但将其中一个做为事件总线.
你可以使用任何[ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties)属性作为连接属性.
**示例: 指定连接端口**
````csharp
{
"RabbitMQ": {
"Connections": {
"Default": {
"HostName": "123.123.123.123",
"Port": "5672"
}
}
}
}
````
### 选项类
`AbpRabbitMqOptions``AbpRabbitMqEventBusOptions` 类用于配置RabbitMQ的连接字符串和事件总线选项.
你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项.
**示例: 配置连接**
````csharp
Configure<AbpRabbitMqOptions>(options =>
{
options.Connections.Default.UserName = "user";
options.Connections.Default.Password = "pass";
options.Connections.Default.HostName = "123.123.123.123";
options.Connections.Default.Port = 5672;
});
````
**示例: 配置客户端和交换机名称**
````csharp
Configure<AbpRabbitMqEventBusOptions>(options =>
{
options.ClientName = "TestApp1";
options.ExchangeName = "TestMessages";
});
````
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.

300
docs/zh-Hans/Distributed-Event-Bus.md

@ -0,0 +1,300 @@
# 分布式事件总线
分布式事件总线系统允许**发布**和**订阅跨应用/服务边界**传输的事件. 你可以使用分布式事件总线在**微服务**或**应用程序**之间异步发送和接收消息.
## 提供程序
分布式事件总线系统提供了一个可以被任何提供程序实现的**抽象**. 有两种开箱即用的提供程序:
* `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**.
* `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-Integration.md)了解如何配置它.
使用本地事件总线作为默认具有一些重要的优点. 最重要的是:它允许你编写与分布式体系结构兼容的代码. 您现在可以编写一个整体应用程序,以后可以拆分成微服务. 最好通过分布式事件而不是本地事件在边界上下文之间(或在应用程序模块之间)进行通信.
例如,[预构建的应用模块](Modules/Index.md)被设计成在分布式系统中作为服务工作,同时它们也可以在独立应用程序中作为模块工作,而不依赖于外部消息代理.
## 发布事件
以下介绍了两种发布分布式事件的方法.
### IDistributedEventBus
可以[注入](Dependency-Injection.md) `IDistributedEventBus` 并且使用发布分布式事件.
**示例: 产品的存货数量发生变化时发布分布式事件**
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly IDistributedEventBus _distributedEventBus;
public MyService(IDistributedEventBus distributedEventBus)
{
_distributedEventBus = distributedEventBus;
}
public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
{
await _distributedEventBus.PublishAsync(
new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
}
);
}
}
}
````
`PublishAsync` 方法需要一个参数:事件对象,它负责保持与事件相关的数据,是一个简单的普通类:
````csharp
using System;
namespace AbpDemo
{
[EventName("MyApp.Product.StockChange")]
public class StockCountChangedEto
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
}
````
即使你不需要传输任何数据也需要创建一个类(在这种情况下为空类).
> `Eto` 是我们按照约定使用的**E**vent **T**ransfer **O**bjects(事件传输对象)的后缀. s虽然这不是必需的,但我们发现识别这样的事件类很有用(就像应用层上的[DTO](Data-Transfer-Objects.md) 一样).
#### 事件名称
`EventName`attribute是可选的,但建议使用. 如果不声明,事件名将事件名称将是事件类的全名. 这里是 `AbpDemo.StockCountChangedEto`.
#### 关于序列化的事件对象
事件传输对象**必须是可序列化**的,因为将其传输到流程外时,它们将被序列化/反序列化为JSON或其他格式.
避免循环引用,多态,私有setter,并提供默认(空)构造函数,如果你有其他的构造函数.(虽然某些序列化器可能会正常工作),就像DTO一样.
### 实体/聚合根类
[实体](Entities.md)不能通过依赖注入注入服务,但是在实体/聚合根类中发布分布式事件是非常常见的.
**示例: 在聚合根方法内发布分布式事件**
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace AbpDemo
{
public class Product : AggregateRoot<Guid>
{
public string Name { get; set; }
public int StockCount { get; private set; }
private Product() { }
public Product(Guid id, string name)
: base(id)
{
Name = name;
}
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
//ADD an EVENT TO BE PUBLISHED
AddDistributedEvent(
new StockCountChangedEto
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
````
`AggregateRoot` 类定义了 `AddDistributedEvent` 来添加一个新的分布式事件,事件在聚合根对象保存(创建,更新或删除)到数据库时发布.
> 如果实体发布这样的事件,以可控的方式更改相关属性是一个好的实践,就像上面的示例一样 - `StockCount`只能由保证发布事件的 `ChangeStockCount` 方法来更改.
#### IGeneratesDomainEvents 接口
实际上添加分布式事件并不是 `AggregateRoot` 类独有的. 你可以为任何实体类实现 `IGeneratesDomainEvents`. 但是 `AggregateRoot` 默认实现了它简化你的工作.
> 不建议为不是聚合根的实体实现此接口,因为它可能不适用于此类实体的某些数据库提供程序. 例如它适用于EF Core,但不适用于MongoDB.
#### 它是如何实现的?
调用 `AddDistributedEvent` 不会立即发布事件. 当你将更改保存到数据库时发布该事件;
* 对于 EF Core, 它在 `DbContext.SaveChanges` 中发布.
* 对于 MongoDB, 它在你调用仓储的 `InsertAsync`, `UpdateAsync``DeleteAsync` 方法时发由 (因为MongoDB没有更改跟踪系统).
## 订阅事件
一个服务可以实现 `IDistributedEventHandler<TEvent>` 来处理事件.
**示例: 处理上面定义的`StockCountChangedEto`**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyHandler
: IDistributedEventHandler<StockCountChangedEto>,
ITransientDependency
{
public async Task HandleEventAsync(StockCountChangedEto eventData)
{
var productId = eventData.ProductId;
}
}
}
````
这就是全部.
* `MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEto` 事件时调用 `HandleEventAsync`.
* 如果你使用的是分布式消息代理,比如RabbitMQ,ABP会自动**订阅消息代理上的事件**,获取消息执行处理程序.
* 如果事件处理程序成功执行(没有抛出任何异常),它将向消息代理发送**确认(ACK)**.
你可以在处理程序注入任何服务来执行所需的逻辑. 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `IDistributedEventHandler<TEvent>` 接口.
> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项.
## 预定义的事件
如果你配置,ABP框架会为[实体](Entities.md)**自动发布创建,更新和删除**分布式事件.
### 事件类型
有三种预定义的事件类型:
* `EntityCreatedEto<T>` 是实体 `T` 创建后发布.
* `EntityUpdatedEto<T>` 是实体 `T` 更新后发布.
* `EntityDeletedEto<T>` 是实体 `T` 删除后发布.
这些都是泛型的, `T` 实际上是**E**vent **T**ransfer **O**bject (ETO)的类型,而不是实体的类型,因为实体对象不能做为事件数据传输,所以通常会为实体类定义一个ETO类,如为 `Product` 实体定义 `ProductEto`.
### 订阅事件
订阅自动事件与订阅常规分布式事件相同.
**示例: 产品更新后获取通知**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Distributed;
namespace AbpDemo
{
public class MyHandler :
IDistributedEventHandler<EntityUpdatedEto<ProductEto>>,
ITransientDependency
{
public async Task HandleEventAsync(EntityUpdatedEto<ProductEto> eventData)
{
var productId = eventData.Entity.Id;
//TODO
}
}
}
````
* `MyHandler` 实现了 `IDistributedEventHandler<EntityUpdatedEto<ProductEto>>`.
### 配置
你可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 中配置 `AbpDistributedEntityEventOptions`添加选择器.
**示例: 配置示例**
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
//Enable for all entities
options.AutoEventSelectors.AddAll();
//Enable for a single entity
options.AutoEventSelectors.Add<IdentityUser>();
//Enable for all entities in a namespace (and child namespaces)
options.AutoEventSelectors.AddNamespace("Volo.Abp.Identity");
//Custom predicate expression that should return true to select a type
options.AutoEventSelectors.Add(
type => type.Namespace.StartsWith("MyProject.")
);
});
````
* 最后一个提供了灵活性来决定是否应该针对给定的实体类型发布事件. 返回 `true` 代表为该 `Type` 发布事件.
你可以添加多个选择器. 如果选择器之一与实体类型匹配,则将其选中.
### 事件传输对象
一旦你为一个实体启用了**自动事件**,ABP框架就会为实体上的更改发布事件. 如果你没有为实体指定对应的**E**vent **T**ransfer **O**bject(ETO), ABP框架会使用一个标准类型 `EntityEto`,它只有两个属性:
* `EntityType` (`string`): 实体类的全名(包括命令空间).
* `KeysAsString` (`string`): 已更改实体的主键.如果它只有一个主键,这个属性将是主键值. 对于复合键,它包含所有用`,`(逗号)分隔的键.
因此可以实现 `IDistributedEventHandler<EntityUpdatedEto<EntityEto>>` 订阅事件. 但是订阅这样的通用事件不是一个好方法,你可以为实体类型定义对应的ETO.
**示例: 为 `Product` 声明使用 `ProductDto`**
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
options.AutoEventSelectors.Add<Product>();
options.EtoMappings.Add<Product, ProductEto>();
});
````
在这个示例中;
* 添加选择器允许发布 `Product` 实体的创建,更新和删除事件.
* 配置为使用 `ProductEto` 作为事件传输对象来发布与 `Product` 相关的事件.
分布式事件系统使用[对象到对象的映射](Object-To-Object-Mapping.md)系统来映射 `Product` 对象到 `ProductEto` 对象,你需要配置映射. 请参阅可以对象到对象映射文档了解所有选项,下面的示例展示了如何使用[AutoMapper](https://automapper.org/)库配置它.
**示例: 使用AutoMapper配置 `Product``ProductEto` 映射**
````csharp
using System;
using AutoMapper;
using Volo.Abp.Domain.Entities.Events.Distributed;
namespace AbpDemo
{
[AutoMap(typeof(Product))]
public class ProductEto : EntityEto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
}
````
此示例使用AutoMapper的 `AutoMap` 属性配置的映射. 你可以创建一个配置文件类代替. 请参阅AutoMapper文档了解更多选项.

4
docs/zh-Hans/Entity-Framework-Core-MySQL.md

@ -8,14 +8,14 @@
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.MySQL;`).
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreMySQLModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.MySQL;`).
## UseMySQL()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseMySQL()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

60
docs/zh-Hans/Entity-Framework-Core-Oracle-Devart.md

@ -0,0 +1,60 @@
# 切换到 EF Core Oracle Devart 提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,有关其他选项,请参见[文档](Entity-Framework-Core-Oracle.md).
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的[Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包.
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
找到 *YourProjectName*MigrationsDbContextFactory.cs 的 `CreateDbContext()` 方法,将以下代码块
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
替换为:
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关Oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator``.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.

67
docs/zh-Hans/Entity-Framework-Core-Oracle-Official.md

@ -0,0 +1,67 @@
# 切换到EF Core Oracle提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包.
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法:
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
与这个
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator``.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.

67
docs/zh-Hans/Entity-Framework-Core-Oracle.md

@ -1,67 +1,10 @@
# 切换到EF Core Oracle提供程序
# 切换到EF Core Oracle 提供程序
本文介绍如何将预配置为SqlServer提供程序的 **[应用程序启动模板](Startup-Templates/Application.md)** 切换到 **Oracle** 数据库提供程序
> 本文档使用[Devart](https://www.devart.com/dotconnect/oracle/)公司的付费库,因为它是oracle唯一支持EF Core 3.x的库
ABP框架提供了两种不同的Oracle包集成. 你可以选择以下其中一个:
## 替换Volo.Abp.EntityFrameworkCore.SqlServer包
* **[Volo.Abp.EntityFrameworkCore.Oracle](Entity-Framework-Core-Oracle-Official.md)** 使用官方 & 免费的oracle驱动 ( **当前处于 beta**).
* **[Volo.Abp.EntityFrameworkCore.Oracle.Devart](Entity-Framework-Core-Oracle-Devart.md)** 使用[Devart](https://www.devart.com/)公司提供的商业(付费)驱动.
解决方案中的 `.EntityFrameworkCore` 项目依赖于 [Volo.Abp.EntityFrameworkCore.SqlServer](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.SqlServer) NuGet包. 删除这个包并且添加相同版本的 [Volo.Abp.EntityFrameworkCore.Oracle.Devart](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart) 包.
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreOracleDevartModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.Oracle.Devart;`).
## UseOracle()
Find `UseSqlServer()` calls in your solution, replace with `UseOracle()`. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project.
* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project.
In the `CreateDbContext()` method of the *YourProjectName*MigrationsDbContextFactory.cs, replace the following code block
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseOracle()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
使用以下代码替换*YourProjectName*MigrationsDbContextFactory.cs中的 `CreateDbContext()` 方法:
```csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
```
与这个
```csharp
var builder = (DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>)
new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>().UseOracle
(
configuration.GetConnectionString("Default")
);
```
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.
## 更改连接字符串
Oracle连接字符串与SQL Server连接字符串不同. 所以检查你的解决方案中所有的 `appsettings.json` 文件,更改其中的连接字符串. 有关oracle连接字符串选项的详细内容请参见[connectionstrings.com](https://www.connectionstrings.com/oracle/).
通常需要更改 `.DbMigrator``.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 重新生成迁移
启动模板使用[Entity Framework Core的Code First迁移](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). EF Core迁移取决于所选的DBMS提供程序. 因此更改DBMS提供程序会导致迁移失败.
* 删除 `.EntityFrameworkCore.DbMigrations` 项目下的Migrations文件夹,并重新生成解决方案.
* 在包管理控制台中运行 `Add-Migration "Initial"`(在解决方案资源管理器选择 `.DbMigrator` (或 `.Web`) 做为启动项目并且选择 `.EntityFrameworkCore.DbMigrations` 做为默认项目).
这将创建一个配置所有数据库对象(表)的数据库迁移.
运行 `.DbMigrator` 项目创建数据库和初始种子数据.
## 运行应用程序
它已准备就绪, 只需要运行该应用程序与享受编码.
> 你可以选择一个你想要的包,如果你不知道它们之间的区别,请在网站上进行搜索. ABP框架仅提供集成,不提供第三库类库的支持.

4
docs/zh-Hans/Entity-Framework-Core-PostgreSQL.md

@ -8,14 +8,14 @@
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.PostgreSql;`).
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.PostgreSql;`).
## UseNpgsql()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseNpgsql()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

4
docs/zh-Hans/Entity-Framework-Core-SQLite.md

@ -8,14 +8,14 @@
## 替换模块依赖项
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (或者替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.Sqlite;`).
`.EntityFrameworkCore` 项目中找到 **YourProjectName*EntityFrameworkCoreModule** 类, 删除 `DependsOn` attribute 上的`typeof(AbpEntityFrameworkCoreSqlServerModule)`, 添加 `typeof(AbpEntityFrameworkCoreSqliteModule)` (并且替换 `using Volo.Abp.EntityFrameworkCore.SqlServer;``using Volo.Abp.EntityFrameworkCore.Sqlite;`).
## UseSqlite()
查找你的解决方案中 `UseSqlServer()`调用,替换为 `UseSqlite()`. 检查下列文件:
* `.EntityFrameworkCore` 项目中的*YourProjectName*EntityFrameworkCoreModule.cs.
* `.EntityFrameworkCore` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
* `.EntityFrameworkCore.DbMigrations` 项目中的*YourProjectName*MigrationsDbContextFactory.cs.
> 根据你的解决方案的结构,你可能发现更多需要改变代码的文件.

11
docs/zh-Hans/Event-Bus.md

@ -1,3 +1,10 @@
# Event Bus
# 事件总线
TODO
事件总线是将消息从发送方传输到接收方的中介. 它在对象,服务和应用程序之间提供了一种松散耦合的通信方式.
## 事件总线类型
ABP框架提供了两种事件总线类型;
* **[本地事件总线](Local-Event-Bus.md)** 适合进程内消息传递.
* **[分布式事件总线](Distributed-Event-Bus.md)** 适合进程间消息传递,如微服务发布和订阅分布式事件.

227
docs/zh-Hans/Local-Event-Bus.md

@ -0,0 +1,227 @@
# 本地事件总线
本地事件总线允许服务发布和订阅**进程内事件**. 这意味着如果两个服务(发布者和订阅者)在同一个进程中运行,那么它是合适的.
## 发布事件
以下介绍了两种发布本地事件的方法.
### ILocalEventBus
可以[注入](Dependency-Injection.md) `ILocalEventBus` 并且使用发布本地事件.
**示例: 产品的存货数量发生变化时发布本地事件**
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
public MyService(ILocalEventBus localEventBus)
{
_localEventBus = localEventBus;
}
public virtual async Task ChangeStockCountAsync(Guid productId, int newCount)
{
//TODO: IMPLEMENT YOUR LOGIC...
//PUBLISH THE EVENT
await _localEventBus.PublishAsync(
new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
}
);
}
}
}
````
`PublishAsync` 方法需要一个参数:事件对象,它负责保持与事件相关的数据,是一个简单的普通类:
````csharp
using System;
namespace AbpDemo
{
public class StockCountChangedEvent
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
}
````
即使你不需要传输任何数据也需要创建一个类(在这种情况下为空类).
### 实体/聚合根类
[实体](Entities.md)不能通过依赖注入注入服务,但是在实体/聚合根类中发布本地事件是非常常见的.
**示例: 在聚合根方法内发布本地事件**
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace AbpDemo
{
public class Product : AggregateRoot<Guid>
{
public string Name { get; set; }
public int StockCount { get; private set; }
private Product() { }
public Product(Guid id, string name)
: base(id)
{
Name = name;
}
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
//ADD an EVENT TO BE PUBLISHED
AddLocalEvent(
new StockCountChangedEvent
{
ProductId = Id,
NewCount = newCount
}
);
}
}
}
````
`AggregateRoot` 类定义了 `AddLocalEvent` 来添加一个新的本地事件,事件在聚合根对象保存(创建,更新或删除)到数据库时发布.
> 如果实体发布这样的事件,以可控的方式更改相关属性是一个好的实践,就像上面的示例一样 - `StockCount`只能由保证发布事件的 `ChangeStockCount` 方法来更改.
#### IGeneratesDomainEvents 接口
实际上添加本地事件并不是 `AggregateRoot` 类独有的. 你可以为任何实体类实现 `IGeneratesDomainEvents`. 但是 `AggregateRoot` 默认实现了它简化你的工作.
> 不建议为不是聚合根的实体实现此接口,因为它可能不适用于此类实体的某些数据库提供程序. 例如它适用于EF Core,但不适用于MongoDB.
#### 它是如何实现的?
调用 `AddLocalEvent` 不会立即发布事件. 当你将更改保存到数据库时发布该事件;
* 对于 EF Core, 它在 `DbContext.SaveChanges` 中发布.
* 对于 MongoDB, 它在你调用仓储的 `InsertAsync`, `UpdateAsync``DeleteAsync` 方法时发由 (因为MongoDB没有更改跟踪系统).
## 订阅事件
一个服务可以实现 `ILocalEventHandler<TEvent>` 来处理事件.
**示例: 处理上面定义的`StockCountChangedEvent`**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
namespace AbpDemo
{
public class MyHandler
: ILocalEventHandler<StockCountChangedEvent>,
ITransientDependency
{
public async Task HandleEventAsync(StockCountChangedEvent eventData)
{
//TODO: your code that does somthing on the event
}
}
}
````
这就是全部,`MyHandler` 由ABP框架**自动发现**,并在发生 `StockCountChangedEvent` 事件时调用 `HandleEventAsync`.
* 事件可以由**0个或多个处理程序**订阅.
* 一个事件处理程序可以**订阅多个事件**,但是需要为每个事件实现 `ILocalEventHandler<TEvent>` 接口.
> 事件处理程序类必须注册到依赖注入(DI),示例中使用了 `ITransientDependency`. 参阅[DI文档](Dependency-Injection.md)了解更多选项.
## 事务和异常行为
当一个事件发布,订阅的事件处理程序将立即执行.所以;
* 如果处理程序**抛出一个异常**,它会影响发布该事件的代码. 这意味着它在 `PublishAsync` 调用上获得异常. 因此如果你想隐藏错误,在事件处理程序中**使用try-catch**.
*如果在一个[工作单元](Unit-Of-Work.md)范围内执行的事件发布的代码,该事件处理程序也由工作单元覆盖. 这意味着,如果你的UOW是事务和处理程序抛出一个异常,事务会回滚.
## 预定义的事件
**发布实体创建,更新,删除事件**是常见的操作. ABP框架为所有的实体**自动**发布这些事件. 你只需要订阅相关的事件.
**示例: 订阅用户创建事件**
````csharp
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
namespace AbpDemo
{
public class MyHandler
: ILocalEventHandler<EntityCreatedEventData<IdentityUser>>,
ITransientDependency
{
public async Task HandleEventAsync(
EntityCreatedEventData<IdentityUser> eventData)
{
var userName = eventData.Entity.UserName;
var email = eventData.Entity.Email;
//...
}
}
}
````
这个类订阅 `EntityCreatedEventData<IdentityUser>`,它在用户创建后发布. 你可能需要向新用户发送一封"欢迎"电子邮件.
这些事件有两种类型:过去时态的事件和进行时态的事件.
### 用过去时态事件
当相关工作单元完成且实体更改成功保存到数据库时,将发布带有过去时态的事件. 如果在这些事件处理程序上抛出异常,则**无法回滚**事务,因为事务已经提交.
事件类型;
* `EntityCreatedEventData<T>` 当实体创建创建成功后发布.
* `EntityUpdatedEventData<T>` 当实体创建更新成功后发布.
* `EntityDeletedEventData<T>` 当实体创建删除成功后发布.
* `EntityChangedEventData<T>` 当实体创建,更新,删除后发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件.
### 用于进行时态事件
带有进行时态的事件在完成事务之前发布(如果数据库事务由所使用的数据库提供程序支持). 如果在这些事件处理程序上抛出异常,它**会回滚**事务,因为事务还没有完成,更改也没有保存到数据库中.
事件类型;
* `EntityCreatingEventData<T>` 当新实体保存到数据库前发布.
* `EntityUpdatingEventData<T>` 当已存在实体更新到数据库前发布.
* `EntityDeletingEventData<T>` 删除实体前发布.
* `EntityChangingEventData<T>` 当实体创建,更新,删除前发布. 如果你需要监听任何类型的更改,它是一种快捷方式 - 而不是订阅单个事件.
#### 它是如何实现的?
在将更改保存到数据库时发布预构建事件;
* 对于 EF Core, 他们在 `DbContext.SaveChanges` 发布.
* 对于 MongoDB, 在你调用仓储的 `InsertAsync`, `UpdateAsync``DeleteAsync` 方法发布(因为MongoDB没有更改追踪系统).

131
docs/zh-Hans/Repositories.md

@ -38,6 +38,8 @@ public class PersonAppService : ApplicationService
}
````
> 参阅 "*IQueryable & 异步操作*" 部分了解如何使用 **异步扩展方法**, 如 `ToListAsync()` (建议始终使用异步) 而不是 `ToList()`.
在这个例子中;
* `PersonAppService` 在它的构造函数中注入了 `IRepository<Person, Guid>` .
@ -119,4 +121,131 @@ public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPe
}
````
你可以直接使用数据库访问提供程序 (本例中是 `DbContext` ) 来执行操作. 有关基于EF Core的自定义仓储的更多信息, 请参阅[EF Core 集成文档](Entity-Framework-Core.md).
你可以直接使用数据库访问提供程序 (本例中是 `DbContext` ) 来执行操作.
> 请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)了解如何自定义仓储.
## IQueryable & 异步操作
`IRepository` 继承自 `IQueryable`,这意味着你可以**直接使用LINQ扩展方法**. 如上面的*泛型仓储*示例.
**示例: 使用 `Where(...)``ToList()` 扩展方法**
````csharp
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToList();
````
`.ToList`, `Count()`... 是在 `System.Linq` 命名空间下定义的扩展方法. ([参阅所有方法](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)).
你通常想要使用 `.ToListAsync()`, `.CountAsync()`.... 来编写**真正的异步代码**.
但在你使用标准的[应用程序启动模板](Startup-Templates/Application.md)时会发现无法在应用层或领域层使用这些异步扩展方法,因为:
* 这里异步方法**不是标准LINQ方法**,它们定义在[Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore)Nuget包中.
* 标准模板应用层与领域层**不引用**EF Core 包以实现数据库提供程序独立.
根据你的需求和开发模式,你可以根据以下选项使用异步方法.s
> 强烈建议使用异步方法! 在执行数据库查询时不要使用同步LINQ方法,以便能够开发可伸缩的应用程序.
### 选项-1: 引用EF Core
最简单的方法是在你想要使用异步方法的项目直接引用EF Core包.
> 添加[Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore)NuGet包到你的项目间接引用EF Core包. 这可以确保你的应用程序其余部分兼容正确版本的EF Core.
当你添加NuGet包后,你可以使用全功能的EF Core扩展方法.
**示例: 直接使用 `ToListAsync()`**
````csharp
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToListAsync();
````
此方法建议;
* 如果你正在开发一个应用程序并且**不打算在将来** 更新FE Core,或者如果以后需要更改,你可以**容忍**它. 如果你正在开发最终的应用程序,这是合理的.
#### MongoDB
如果使用的是MongoDB,则需要将[Volo.Abp.MongoDB] NuGet包添加到项目中. 但在这种情况下你也不能直接使用异步LINQ扩展(例如`ToListAsync`),因为MongoDB不提供 `IQueryable<T>`的异步扩展方法,而是提供 `IMongoQueryable<T>`. 你需要先将查询强制转换为 `IMongoQueryable<T>` 才能使用异步扩展方法.
**示例: 转换Cast `IQueryable<T>``IMongoQueryable<T>` 并且使用 `ToListAsync()`**
````csharp
var people = ((IMongoQueryable<Person>)_personRepository
.Where(p => p.Name.Contains(nameFilter)))
.ToListAsync();
````
### 选项-2: 自定义仓储方法
你始终可以创建自定义仓储方法并使用特定数据库提供程序的API,比如这里的异步扩展方法. 有关自定义存储库的更多信息,请参阅[EF Core](Entity-Framework-Core.md)或[MongoDb](MongoDB.md)文档.
此方法建议;
* 如果你想**完全隔离**你的领域和应用层和数据库提供程序.
* 如果你开发可**重用的[应用模块](Modules/Index.md)**,并且不想强制使用特定的数据库提供程序,这应该作为一种[最佳实践](Best-Practices/Index.md).
### 选项-3: IAsyncQueryableExecuter
> 注意,此功能在ABP框架3.0以之后的版本可用,虽然它也可以用于较早的版本,但它提供的方法非常有限.
`IAsyncQueryableExecuter` 是一个用于异步执行 `IQueryable<T>` 对象的服务,**不依赖于实际的数据库提供程序**.
**示例: 注入并使用 `IAsyncQueryableExecuter.ToListAsync()` 方法**
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
namespace AbpDemo
{
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly IAsyncQueryableExecuter _asyncExecuter;
public ProductAppService(
IRepository<Product, Guid> productRepository,
IAsyncQueryableExecuter asyncExecuter)
{
_productRepository = productRepository;
_asyncExecuter = asyncExecuter;
}
public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Create the query
var query = _productRepository
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);
//Run the query asynchronously
List<Product> products = await _asyncExecuter.ToListAsync(query);
//...
}
}
}
````
> `ApplicationService``DomainService` 基类已经预属性注入了 `AsyncExecuter` 属性,所以你可直接使用.
ABP框架使用实际数据库提供程序的API异步执行查询.虽然这不是执行查询的常见方式,但它是使用异步API而不依赖于数据库提供者的最佳方式.
此方法建议;
* 如果你正在构建一个没有数据库提供程序集成包的**可重用库**,但是在某些情况下需要执行 `IQueryable<T>`对象.
例如,ABP框架在 `CrudAppService` 基类中(参阅[应用程序](Application-Services.md)文档)使用 `IAsyncQueryableExecuter`.

3
docs/zh-Hans/SignalR-Integration.md

@ -234,4 +234,5 @@ ABP框架不会更改SignalR. 就像在其他ASP.NET Core应用程序中一样,
## 另请参阅
* [微软SignalR文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction)
* [微软SignalR文档](https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction)
* [使用ABP,SignalR和RabbitMQ在分布式体系结构中的实时消息传递](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ)

224
docs/zh-Hans/Tutorials/Part-1.md

@ -737,8 +737,8 @@ $(function () {
是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具:
- [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务.
- [NGXS](https://ngxs.gitbook.io/ngxs/) 用于管理状态库.
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库.
- [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器).
#### 安装 NPM 包
@ -761,26 +761,56 @@ yarn ng generate module book --routing true
#### 路由
打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的 `import`路由:
打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的路由:
```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
const routes: Routes = [
// ...
// added a new route to the routes array
{
path: 'books',
loadChildren: () => import('./book/book.module').then(m => m.BookModule)
}
]
```
//...added book path with the below to the routes array
{
path: 'book',
component: ApplicationLayoutComponent,
loadChildren: () => import('./book/book.module').then(m => m.BookModule),
data: {
routes: {
name: '::Menu:Book',
iconClass: 'fas fa-book'
} as ABP.Route
},
* 我们添加了一个懒加载路由. 参阅 [嬾加載功能模块](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules).
打开位于 `src\app` 目录下的 `route.provider.ts` 文件,用以下内容替换它:
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
//...
// added below element
{
path: '/books',
name: '::Menu:Books',
iconClass: 'fas fa-book',
order: 101,
layout: eLayoutType.application,
},
]);
};
}
```
* `ApplicationLayoutComponent` 配置将应用程序布局设置为新页面, 我们添加了 `data` 对象. `name` 是菜单项的名称,`iconClass` 是菜单项的图标.
* 我们添加了一个新的路由元素在菜单上显示为 "Books" 的导航元素.
* `path` 路由的URL.
* `name` 菜单项的名称,可以使用本地化Key.
* `iconClass` 菜单项的图标.
* `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面.
* `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account``eLayoutType.empty`.
更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice).
#### Book 列表组件
@ -848,35 +878,6 @@ yarn start
![Initial book list page](./images/bookstore-initial-book-list-page.png)
#### 创建 BookState
运行以下命令创建名为 `BookState` 的新state:
```bash
npx @ngxs/cli --name book --directory src/app/book
```
* 此命令在 `src/app/book/state` 文件夹下创建了 `book.state.ts``book.actions.ts` 文件. 参阅 [NGXS CLI文档](https://www.ngxs.io/plugins/cli)了解更多.
`BookState` 导入到 `src/app` 文件夹中的 `app.module.ts` 中. 然后添加 `BookState``NgxsModule``forRoot` 静态方法,作为该方法的第一个参数的数组元素.
```js
// ...
import { BookState } from './book/state/book.state'; //<== imported BookState ==>
@NgModule({
imports: [
// other imports
NgxsModule.forRoot([BookState]), //<== added BookState ==>
//other imports
],
// ...
})
export class AppModule {}
```
#### 生成代理
ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md).
@ -893,109 +894,41 @@ abp generate-proxy --module app
![Generated files](./images/generated-proxies.png)
#### GetBook 动作
动作可以被认为是一个命令,它应该触发某些事情发生,或者是已经发生的事情的结果事件.[See NGXS Actions文档](https://www.ngxs.io/concepts/actions).
打开 `app/book/state` 目录下的 `book.actions.ts` 文件用以下内容替换它:
```js
export class GetBook {
static readonly type = '[Book] Get';
}
```
#### 实现 BookState
打开 `app/book/state` 目录下的 `book.state.ts` 文件用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks } from './book.actions';
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
}
```
* 我们添加了book属性到BookStateModel模态框.
* 我们添加了 `GetBook` 动作. 它通过 ABP CLI生成的 `BookService` 检索图书数据.
* `NGXS` 需要在不订阅get函数的情况下返回被观察对象.
#### BookListComponent
打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
loading = false;
constructor(private store: Store) {}
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
}
```
* 我们添加了 `get` 函数获取book更新store.
* 有关 `NGXS` 特性的更多信息请参见NGXS文档中的[Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions)和[Select](https://ngxs.gitbook.io/ngxs/concepts/select).
* 我们注入了生成的 `BookService`.
* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索.
打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它:
@ -1005,38 +938,31 @@ export class BookListComponent implements OnInit {
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ "::Menu:Book" | abpLocalization }}}%}
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
</div>
</div>
<div class="card-body">
<abp-table
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
</div>
```

606
docs/zh-Hans/Tutorials/Part-2.md

@ -454,78 +454,48 @@ $(function () {
下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体.
#### 状态定义
#### 添加 modal 到 BookListComponent
`app\book\state` 文件夹下打开 `book.action.ts` 文件,使用以下内容替换它:
```js
import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Book] Get';
}
Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below:
// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto) { }
}
```
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from '../models';
import { BookService } from '../services';
* 我们导入了 `CreateUpdateBookDto` 模型并且创建了 `CreateUpdateBook` 动作.
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它:
booksType = BookType;
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
isModalOpen = false; // <== added this line ==>
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
constructor(public readonly list: ListService, private bookService: BookService) {}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((bookResponse) => {
ctx.patchState({
book: bookResponse,
});
})
);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
// added CreateUpdateBook action listener
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
return this.bookService.createByInput(action.payload);
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* 我们导入了 `CreateUpdateBook` 动作并且定义了 `save` 方法监听 `CreateUpdateBook` 动作去创建图书.
`SaveBook` 动作被分派时,save方法被执行. 它调用 `BookService``createByInput` 方法.
#### 添加模态到 BookListComponent
* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法.
打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它:
@ -534,19 +504,12 @@ export class BookState {
<div class="card-header">
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
<h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
</div>
<!--Added new book button -->
<!--Added new book button -->
<div class="text-right col col-md-6">
<div class="text-lg-right pt-2">
<button
id="create"
class="btn btn-primary"
type="button"
(click)="createBook()"
>
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button>
@ -555,47 +518,40 @@ export class BookState {
</div>
</div>
<div class="card-body">
<abp-table
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
</div>
<!--added modal-->
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template>
<ng-template #abpHeader>
<h3>{%{{{ '::NewBook' | abpLocalization }}}%}</h3>
</ng-template>
<ng-template #abpBody> </ng-template>
<ng-template #abpBody> </ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpAccount::Close' | abpLocalization }}}%}
</button>
</ng-template>
</abp-modal>
```
@ -603,55 +559,6 @@ export class BookState {
* `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处.
* 我们添加了 `New book` 按钮到 `AbpContentToolbar`.
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
booksType = BookType;
loading = false;
isModalOpen = false; // <== added this line ==>
constructor(private store: Store) {}
ngOnInit() {
this.get();
}
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
}
// added createBook method
createBook() {
this.isModalOpen = true;
}
}
```
* 我们添加了 `isModalOpen = false``createBook` 方法.
你可以打开浏览器,点击**New book**按钮看到模态框.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
@ -660,51 +567,46 @@ export class BookListComponent implements OnInit {
[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [ListService],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
loading = false;
isModalOpen = false;
form: FormGroup; // <== added this line ==>
constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder // <== injected FormBuilder ==>
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
this.buildForm(); //<== added this line ==>
this.buildForm(); // <== added this line ==>
this.isModalOpen = true;
}
@ -729,7 +631,7 @@ export class BookListComponent implements OnInit {
#### 创建表单的DOM元素
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
```html
<ng-template #abpBody>
@ -748,7 +650,7 @@ export class BookListComponent implements OnInit {
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypeArr"> {%{{{ type }}}%}</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option>
</select>
</div>
@ -798,16 +700,13 @@ export class BooksModule { }
* 我们导入了 `NgbDatepickerModule` 来使用日期选择器.
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@ -815,37 +714,34 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==>
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
//added bookTypeArr array
bookTypeArr = Object.keys(BookType).filter(
// <== added bookTypes array ==>
bookTypes = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {}
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
@ -868,7 +764,7 @@ export class BookListComponent implements OnInit {
* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为Date类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview).
* 我们添加了 `bookTypeArr` 数组,以便能够在combobox值中使用它. `bookTypeArr` 包含 `BookType` 枚举的字段. 得到的数组如下所示:
* 我们添加了 `bookTypes` 数组,以便能够在combobox值中使用它. `bookTypes` 包含 `BookType` 枚举的字段. 得到的数组如下所示:
```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
@ -882,16 +778,13 @@ export class BookListComponent implements OnInit {
#### 保存图书
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -899,36 +792,33 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
booksType = BookType;
bookTypeArr = Object.keys(BookType).filter(
bookTypes = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
constructor(private store: Store, private fb: FormBuilder) {}
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
@ -951,19 +841,18 @@ export class BookListComponent implements OnInit {
return;
}
this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
this.bookService.createByInput(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
this.list.get();
});
}
}
```
* 我们导入了 `CreateUpdateBook`.
* 我们添加了 `save` 方法.
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list`文件, 添加 `abp-button` 保存图书.
打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件, 找到 `<ng-template #abpFooter>` 元素,使用下面元素替换它:
```html
<ng-template #abpFooter>
@ -994,90 +883,49 @@ export class BookListComponent implements OnInit {
### 更新图书
#### CreateUpdateBook 动作
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件,使用以下内容替换它:
```js
import { CreateUpdateBookDto } from '../models';
export class GetBooks {
static readonly type = '[Book] Get';
}
export class CreateUpdateBook {
static readonly type = '[Book] Create Update Book';
constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==>
}
```
* 我们在 `CreateUpdateBook` 动作的构造函数添加了 `id` 参数.
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换 `save` 方法:
```js
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
```
#### BookListComponent
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,在构造函数注入 `BookService` 服务,并添加 名为 `selectedBook` 的变量.
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件并且添加名为 `selectedBook` 的变量.
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { BookDto, BookType } from '../models';
import { GetBooks, CreateUpdateBook } from '../state/book.actions';
import { BookState } from '../state/book.state';
import { BookService } from '../services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { BookService } from '../services'; // <== imported BookService ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BookState.getBooks)
book$: Observable<BookDto[]>;
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
bookType = BookType;
booksType = BookType;
bookTypeArr = Object.keys(BookType).filter(
(bookType) => typeof this.bookType[bookType] === 'number'
bookTypes = Object.keys(BookType).filter(
(bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
isModalOpen = false;
form: FormGroup;
selectedBook = {} as BookDto; // <== declared selectedBook ==>
constructor(private store: Store, private fb: FormBuilder, private bookService: BookService) {} //<== injected BookService ==>
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder
) {}
ngOnInit() {
this.get();
}
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
get() {
this.loading = true;
this.store
.dispatch(new GetBooks())
.pipe(finalize(() => (this.loading = false)))
.subscribe(() => {});
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
// <== this method is replaced ==>
@ -1109,58 +957,46 @@ export class BookListComponent implements OnInit {
});
}
// <== this method is replaced ==>
save() {
if (this.form.invalid) {
return;
}
//<== added this.selectedBook.id ==>
this.store
.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.get();
});
// <== added request ==>
const request = this.selectedBook.id
? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id)
: this.bookService.createByInput(this.form.value);
request.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
}
```
* 我们导入了 `BookService`.
* 我们声明了类型为 `BookDto``selectedBook` 变量.
* 我们在构造函数注入了 `BookService`, 它用于检索正在编辑的图书数据.
* 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象.
* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单.
* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象.
* 我们`CreateUpdateBook` 构造函数添加了 `selectedBook.id`.
* 我们替换了 `save` 方法.
#### 添加 "Actions" 下拉框到表格
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签:
打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<div class="card-body">` 标签:
```html
<div class="card-body">
<abp-table
[value]="book$ | async"
[abpLoading]="loading"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[rows]="10"
[scrollable]="true"
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th>{%{{{ "::Actions" | abpLocalization }}}%}</th>
<th>{%{{{ "::Name" | abpLocalization }}}%}</th>
<th>{%{{{ "::Type" | abpLocalization }}}%}</th>
<th>{%{{{ "::PublishDate" | abpLocalization }}}%}</th>
<th>{%{{{ "::Price" | abpLocalization }}}%}</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>
<ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
<!-- added actions column -->
<ngx-datatable-column
[name]="'::Actions' | abpLocalization"
[maxWidth]="150"
[sortable]="false"
>
<ng-template let-row="row" ngx-datatable-cell-template>
<div ngbDropdown container="body" class="d-inline-block">
<button
class="btn btn-primary btn-sm dropdown-toggle"
@ -1168,25 +1004,38 @@ export class BookListComponent implements OnInit {
aria-haspopup="true"
ngbDropdownToggle
>
<i class="fa fa-cog mr-1"></i>{%{{{ "::Actions" | abpLocalization }}}%}
<i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(data.id)">
{%{{{ "::Edit" | abpLocalization }}}%}
<button ngbDropdownItem (click)="editBook(row.id)">
{%{{{ '::Edit' | abpLocalization }}}%}
</button>
</div>
</div>
</td>
<td>{%{{{ data.name }}}%}</td>
<td>{%{{{ bookType[data.type] }}}%}</td>
<td>{%{{{ data.publishDate | date }}}%}</td>
<td>{%{{{ data.price }}}%}</td>
</tr>
</ng-template>
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ booksType[row.type] }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.publishDate | date }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ row.price | currency }}}%}
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
</div>
```
- 我们添加了 "Actions" 栏的 `th`.
- 我们为 "Actions" 栏添加了 `ngx-datatable-column`.
- 我们添加了带有 `ngbDropdownToggle``button`,在点击按钮时打开操作.
- 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单.
@ -1194,7 +1043,7 @@ UI最终看起来像这样:
![Action buttons](./images/bookstore-actions-buttons.png)
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签:
打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `<ng-template #abpHeader>` 标签:
```html
<ng-template #abpHeader>
@ -1206,80 +1055,9 @@ UI最终看起来像这样:
### 删除图书
#### DeleteBook 动作
打开 `app\book\state` 文件夹下的 `book.actions.ts` 文件添加名为 `DeleteBook` 的动作.
```js
export class DeleteBook {
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
打开 `app\book\state` 文件夹下的 `book.state.ts` 文件,使用以下内容替换它:
```js
import { PagedResultDto } from '@abp/ng.core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==>
import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { BookDto } from '../models';
export class BookStateModel {
public book: PagedResultDto<BookDto>;
}
@State<BookStateModel>({
name: 'BookState',
defaults: { book: {} } as BookStateModel,
})
@Injectable()
export class BookState {
@Selector()
static getBooks(state: BookStateModel) {
return state.book.items || [];
}
constructor(private bookService: BookService) {}
@Action(GetBooks)
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
book: booksResponse,
});
})
);
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
return this.bookService.createByInput(action.payload);
}
}
// <== added DeleteBook action listener ==>
@Action(DeleteBook)
delete(ctx: StateContext<BookStateModel>, action: DeleteBook) {
return this.bookService.deleteById(action.id);
}
}
```
- 我们导入了 `DeleteBook` .
- 我们在文件末尾添加了 `DeleteBook` 动作监听器.
#### 删除确认弹层
打开 `app\app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
替换构造函数:
@ -1288,50 +1066,46 @@ import { ConfirmationService } from '@abp/ng.theme.shared';
//...
constructor(
private store: Store,
private fb: FormBuilder,
private bookService: BookService,
private confirmation: ConfirmationService // <== added this line ==>
) { }
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService // <== added this line ==>
) {}
```
* 我们导入了 `ConfirmationService`.
* 我们在构造函数注入了 `ConfirmationService` .
* We imported `ConfirmationService`.
* We injected `ConfirmationService` to the constructor.
参阅[确认弹层文档](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)了解更多
See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)
`book-list.component.ts` 中添加删除方法:
In the `book-list.component.ts` add a delete method:
```js
import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==>
//...
delete(id: string) {
this.confirmation
.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
.subscribe(status => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
}
});
this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.deleteById(id).subscribe(() => this.list.get());
}
});
}
```
`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下:
`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时调用 `BookService``deleteById` 方法. 确认弹层看起来如下:
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
#### 添加删除按钮
打开 `app\app\book\book-list` 文件夹下的 `app\app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
打开 `app\book\book-list` 文件夹下的 `app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
```html
<div ngbDropdownMenu>
<!-- added Delete button -->
<button ngbDropdownItem (click)="delete(data.id)">
<button ngbDropdownItem (click)="delete(row.id)">
{%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
</button>
</div>

BIN
docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 25 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-book-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 56 KiB

89
docs/zh-Hans/UI/Angular/List-Service.md

@ -53,57 +53,38 @@ class BookComponent {
> 注意 `list``public` 并且 `readonly`. 因为我们将直接在组件的模板中使用 `ListService` 属性. 可以视为反模式,但是实现起来要快得多. 你可以改为使用公共组件属性.
`ListService` 属性放入模板中,如下所示:
像这样绑定 `ListService` 到 ngx-datatable:
```html
<abp-table
[value]="book.items"
[(page)]="list.page"
[rows]="list.maxResultCount"
[totalRecords]="book.totalCount"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[abpLoading]="list.isLoading$ | async"
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
</abp-table>
<ng-template #tableHeader>
<tr>
<th (click)="nameSort.sort('name')">
{%{{{ '::Name' | abpLocalization }}}%}
<abp-sort-order-icon
#nameSort
sortKey="name"
[(selectedSortKey)]="list.sortKey"
[(order)]="list.sortOrder"
></abp-sort-order-icon>
</th>
</tr>
</ng-template>
<ng-template #tableBody let-data>
<tr>
<td>{%{{{ data.name }}}%}</td>
</tr>
</ng-template>
<!-- column templates here -->
</ngx-datatable>
```
## 与Observables一起使用
你可以将Observables与Angular的[AsyncPipe](https://angular.io/guide/observables-in-angular#async-pipe)结合使用:
```ts
```js
book$ = this.list.hookToQuery(query => this.bookService.getListByInput(query));
```
```html
<!-- simplified representation of the template -->
<abp-table
[value]="(book$ | async)?.items || []"
[totalRecords]="(book$ | async)?.totalCount"
<ngx-datatable
[rows]="(book$ | async)?.items || []"
[count]="(book$ | async)?.totalCount || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
<!-- DO NOT WORRY, ONLY ONE REQUEST WILL BE MADE -->
```
@ -111,7 +92,7 @@ class BookComponent {
...or...
```ts
```js
@Select(BookState.getBooks)
books$: Observable<BookDto[]>;
@ -126,13 +107,18 @@ class BookComponent {
```html
<!-- simplified representation of the template -->
<abp-table
[value]="books$ | async"
[totalRecords]="bookCount$ | async"
<ngx-datatable
[rows]="(books$ | async) || []"
[count]="(bookCount$ | async) || 0"
[list]="list"
default
>
</abp-table>
<!-- column templates here -->
</ngx-datatable>
```
> 我们不建议将NGXS存储用于CRUD页面,除非你的应用程序需要在组件之间共享列表信息或稍后在另一页面中使用它.
## 如何在创建/更新/删除时刷新表
`ListService` 公开了一个 `get` 方法来触发当前查询的请求. 因此基本上每当创建,更新或删除操作解析时,你可以调用 `this.list.get();` 它会调用钩子流创建者.
@ -161,3 +147,26 @@ this.bookService.createByInput(form.value)
<input type="text" name="search" [(ngModel)]="list.filter">
```
## ABP v3.0的重大更改
我们必须修改 `ListService` 使其与 `ngx-datatable` 一起使用. 之前 `page` 属性的最小值为 `1`, 你可以像这样使用它:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[(page)]="list.page"
></abp-table>
```
从v3.0开始, 对于`ngx-datatable`, 初始页面的 `page`属性必须设置为 `0`. 因此如果你以前在表上使用过 `ListService` 并打算保留 `abp-table`,则需要进行以下更改:
```html
<!-- other bindings are hidden in favor of brevity -->
<abp-table
[page]="list.page + 1"
(pageChange)="list.page = $event - 1"
></abp-table>
```
**重要提示:** `abp-table` 没有被删除,但是会被弃用,并在将来的版本中移除,请考虑切换到 ngx-datatable.

363
docs/zh-Hans/UI/Angular/Migration-Guide-v3.md

@ -0,0 +1,363 @@
# Angular UI v2.9 迁移到 v3.0 指南
## 在v3.0改变了什么?
### Angular 10
新的ABP Angular UI基于Angular 10和TypeScript 3.9,我们已经放弃了对Angular 8的支持. 不过ABP模块将继续与Angular 9兼容使用. 因此如果你的项目是Angular 9,则无需更新为 Angular10. 更新通常很容易.
#### 如何迁移?
在你的根文件夹中打开一个终端,然后运行以下命令:
```sh
yarn ng update @angular/cli @angular/core --force
```
这会做如下修改:
- 更新你的package.json并安装新的软件包
- 修改tsconfig.json文件创建一个"Solution Style"配置
- 重命名 `browserlist``.browserlistrc`
另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表.
![Table of packages to update](./images/table-of-packages-to-update.png)
当Angular报告上面的包后,运行命令:
```sh
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
```
> 如果Angular提示你的仓库有中未提交的更改,可以提交/存储它,也可以在命令中添加 `--allow-dirty` 参数.
### 配置模块
在ABP v2.x中,每个延迟加载的模块都有一个可通过单独的程序包使用的配置模块,模块配置如下:
```js
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
...在app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
```
虽然有效,但有一些缺点:
- 每个模块都有两个独立的程序包,但实际上这些程序包是相互依赖的.
- 配置延迟加载的模块需要包装模块.
- ABP Commercial具有可扩展性系统,在根模块上配置可扩展模块会增加 bundle 的大小.
在ABP v3.0中,我们为每个配置模块引入了辅助入口点,并且提供了一种在没有包装的情况下配置延迟加载的模块的新方法. 现在模块配置如下所示:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
... 在app-routing.module.ts...
```js
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
```
这项更改帮助我们减少了捆绑包的大小并大大缩短了构建时间. 我们相信你会注意到你的应用程序有所不同.
#### 一个更好的例子
AppModule:
```js
import { AccountConfigModule } from '@abp/ng.account/config';
import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management/config';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
```
AppRoutingModule:
```js
import { DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module')
.then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity')
.then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () => import('@abp/ng.tenant-management')
.then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () => import('@abp/ng.setting-management')
.then(m => m.SettingManagementModule.forLazy()),
},
],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情.
#### 如何迁移?
- 使用 `yarn remove` 删除你的项目的配置包.
- 从辅助入口点(例如`@abp/ng.identity/config`)导入配置模块.
- 调用所有新配置模块的静态 `forRoot`方法,即使配置没有被传递.
- 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容).
- 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`).
- 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递.
- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX.
### RoutesService
在ABP v2.x中,通过以下两种方式之一将路由添加到菜单:
- [通过 `AppRoutingModule` 的 `routes` 属性](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-routes-property-in-approutingmodule)
- [通过 ConfigState](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#via-configstate)
从v3.0开始,我们更改了添加和修改路由的方式. 我们不再将路由存储在 `ConfigState`中(破坏性更改). 而是有一个名为 `RoutesService` 的新服务,该服务用于添加,修补或删除菜单项. 详情请查看[文档](Modifying-the-Menu.md).
#### 如何迁移?
- 检查你是否曾经使用 `ConfigState``ConfigStateService` 添加路由. 请用 `RoutesService``add` 方法替换它们.
- 检查你是否曾经修补的路由. 将其替换为 `RoutesService``patch` 方法.
- 仔细检查你是否使用绝对路径,并在 `add``patch` 方法调用中为子菜单项提供 `parentName` 而不是 `children` 属性.
### NavItemsService
在ABP v2.x中,[通过LayoutStateService](https://docs.abp.io/en/abp/2.9.0/UI/Angular/Modifying-the-Menu#how-to-add-an-element-to-right-part-of-the-menu)添加导航元素.
从v3.0开始,我们改变了添加和修改导航项的方式,以前的方法不再可用(破坏性更改). 详情请查看[文档](Modifying-the-Menu.md).
#### 如何迁移?
- 用 `NavItemsService``addItems` 方法替换所有 `dispatchAddNavigationElement` 调用.
### ngx-datatable
在v3之前,我们一直使用自定义组件 `abp-table` 作为默认表. 但是数据表是复杂的组件,要实现功能齐全的数据表需要大量的精力,我们计划将其引入其他功能.
从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用.
为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容:
```json
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
}
```
由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective``NgxDatatableDefaultDirective`.
这些指令的用法很简单:
```js
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
```
...在组件模板...
```html
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
```
通过 `NgxDatatableListDirective` 绑定注入的 `ListService` 实例后,你不再需要担心分页或排序. 同样 `NgxDatatableDefaultDirective` 去除了几个属性绑定,以使ngx-datatable适合我们的样式.
#### 一个更好的例子
```html
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{%{{{ row.someOtherProp }}}%}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
```
#### 如何迁移?
- 安装 `@swimlane/ngx-datatable` 包.
- 添加ngx-datatable样式到angular.json文件.
- 如果可以的话,根据上面的例子更新你的模.
- 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页.
**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。
### 过时的接口
某些接口早已被标记为已弃用,现在已将其删除.
#### 如何迁移?
请检查你是否仍在使用[Issue中列出的任何内容](https://github.com/abpframework/abp/issues/4281).
## 下一步是什么?
* [服务代理](Service-Proxies.md)

220
docs/zh-Hans/UI/Angular/Modifying-the-Menu.md

@ -1,6 +1,5 @@
# 修改菜单
菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局.
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.-->
@ -24,7 +23,97 @@ export const environment = {
## 如何添加导航元素
### 通过 AppRoutingModule 中的 `routes` 属性
### 通过 `RoutesService`
你可以通过调用 `RoutesService``add` 方法添加路由到菜单,它是一个单例的服务,在root中提供,你可以立即注入使用它.
```js
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component } from '@angular/core';
@Component(/* component metadata */)
export class AppComponent {
constructor(routes: RoutesService) {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
]);
}
}
```
另一种方法是使用路由提供程序. 首先创建一个提供程序:
```js
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/your-path',
name: 'Your navigation',
requiredPolicy: 'permission key here',
order: 101,
iconClass: 'fas fa-question-circle',
layout: eLayoutType.application,
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
requiredPolicy: 'permission key here',
order: 1,
},
]);
};
}
```
...然后在app.module.ts ...
```js
import { NgModule } from '@angular/core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
// imports, declarations, and bootstrap
})
export class AppModule {}
```
下面是每个属性的工作原理:
- `path` 是导航元素的绝对路径.
- `name` 是导航元素的label. 可以使用本地化Key和本地化对象.
- `parentName` 是菜单中父路由的 `name` 的引用,用于创建多级菜单项.
- `requiredPolicy` 是用于访问该页面的权限Key. 参阅[权限管理文档](Permission-Management.md).
- `order` 是导航元素的排序. `Administration` 的顺序是 `100`. 在排序top级别菜单项时请记得这一点.
- `iconClass``i` 标签的class, 它放在导航label的左边.
- `layout` 定义路由使用哪个布局加载. (默认: `eLayoutType.empty`).
- `invisible` 使该项在菜单中不可见. (默认: `false`).
### 通过 `AppRoutingModule``routes` 属性
你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上.
@ -36,7 +125,7 @@ export const environment = {
data: {
routes: {
name: 'Your navigation',
order: 3,
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
children: [
@ -47,100 +136,77 @@ export const environment = {
requiredPolicy: 'permission key here',
},
],
} as ABP.Route, // can be imported from @abp/ng.core
}
},
},
}
```
- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象.
- `order` 排序导航元素.
- `iconClass``i` 标签的类,在导航标签的左侧.
- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./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.
- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`.
添加了上面描述的route属性后,导航菜单如下图所示:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## 通过 ConfigState
`ConfigStateService``dispatchAddRoute` 方法可以向菜单添加一个新的导航元素.
```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
```
`newRoute` 放在根级别,没有任何父路由,url将为`/path`.
如果你想 **添加子路由, 你可以这样做:**
或者你可以这样做:
```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
{
path: 'your-path',
data: {
routes: [
{
path: '/your-path',
name: 'Your navigation',
order: 101,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
order: 1,
requiredPolicy: 'permission key here',
},
] as ABP.Route[], // can be imported from @abp/ng.core
},
}
```
`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`.
第二种方法的优点是你不必绑定到父/子结构,可以使用任何喜欢的路由.
新路由看起来像这样:
如上所述添加 `routes` 属性后,导航菜单看起来像这样:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png)
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## 如何修改一个导航元素
## 如何修补或删除导航元素
`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置.
`RoutesService``patch` 方法通过名称查找路由,并将配置替换为第二个参数传递的新配置. `remove` 方法会找到一个路由并将其连同其子路由一起删除.
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
// this.routes is instance of RoutesService
// eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
const dashboardRouteConfig: ABP.Route = {
path: '/dashboard',
name: '::Menu:Dashboard',
parentName: '::Menu:Home',
order: 1,
layout: eLayoutType.application,
};
const newRouteConfig: Partial<ABP.Route> = {
const newHomeRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration,
parentName: eThemeSharedRouteNames.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
this.routes.add([dashboardRouteConfig]);
this.routes.patch('::Menu:Home', newHomeRouteConfig);
this.routes.remove(['Your navigation']);
```
* 根据给定的 `parentName`_Home_ 导航移动到 _Administration_ 下拉框下.
* 添加了 icon.
* 指定了顺序.
* 添加了名为 _Dashboard_ 的子路由.
- 根据给定的 `parentName`_Home_ 导航移动到 _Administration_ 下拉菜单下.
- 为 _Home_ 添加了图标.
- 指定 _Home_ 的顺序为列表的第一项.
- 为 _Home_ 添加了一个名为 _Dashboard_ 的子路由.
- 删除 _Your navigation_ 与其子路由.
修改后,导航元素看起来像这样:
上述操作后,新的菜单看起来如下:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)
@ -184,7 +250,7 @@ export class AppComponent {
}
```
上面我们在菜单添加了一个搜索输入,最终UI如下:s
上面我们在菜单添加了一个搜索输入,最终UI如下:
![navigation-menu-search-input](./images/navigation-menu-search-input.png)

8
docs/zh-Hans/UI/Angular/Permission-Management.md

@ -8,7 +8,7 @@
```js
import { Store } from '@ngxs/store';
import { ConfigState } from '../states';
import { ConfigState } from '@abp/ng.core';
export class YourComponent {
constructor(private store: Store) {}
@ -24,7 +24,7 @@ export class YourComponent {
或者你可以通过 `ConfigStateService` 获取它:
```js
import { ConfigStateService } from '../services/config-state.service';
import { ConfigStateService } from '@abp/ng.core';
export class YourComponent {
constructor(private configStateService: ConfigStateService) {}
@ -42,7 +42,7 @@ export class YourComponent {
你可以使用 `PermissionDirective` 来根据用户的权限控制DOM元素是否可见.
```html
<div *abpPermission="AbpIdentity.Roles">
<div *abpPermission="'AbpIdentity.Roles'">
仅当用户具有`AbpIdentity.Roles`权限时,此内容才可见.
</div>
```
@ -58,6 +58,8 @@ export class YourComponent {
`requiredPolicy` 添加到路由模块中的 `routes`属性.
```js
import { PermissionGuard } from '@abp/ng.core';
// ...
const routes: Routes = [
{
path: 'path',

BIN
docs/zh-Hans/UI/Angular/images/table-of-packages-to-update.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

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

@ -1,3 +1,277 @@
## Dynamic Forms
# 动态表单
目前还没有文档. 你现在可以看到[组件演示](http://bootstrap-taghelpers.abp.io/Components/DynamicForms).
`提示:` 在开始阅读本文档之前,请确保你已经看过并理解了[abp表单元素](Form-elements.md)文档.
## 介绍
`abp-dynamic-form` 为给定c#模型创建bootstrap表单.
基本用法:
````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
参阅 [动态表单demo页面](https://bootstrap-taghelpers.abp.io/Components/Dropdowns)查看示例.
## Attributes
### abp-model
为动态表单设置c#模型,模型的属性以表单形式转化为输入.
### submit-button
可以为 `True``False`.
如果为 `True`,则会在表单底部生成一个提交按钮.
默认值是 `False`.
### required-symbols
可以为 `True``False`.
如果为 `True`,则必需的输入将带有一个符号(*),表示它们是必需的.
默认值是 `True`.
## 表单内容布局
默认情况下,“`abp-dynamic-form` 会清除内部html并将inputs放入自身. 如果要向动态表单添加其他内容或将inputs放置到某些特定区域,可以使用`<abp-form-content />`标签. 这个标签将被表单内容替换, 而 `abp-dynamic-form` 标签的内部html的其余部分将保持不变.
用法:
````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>
````
## 输入排序
`abp-dynamic-form` 通过 `DisplayOrder` attribute对属性进行排序,然后按模型类中的属性顺序进行排序.
默认每个属性的 `DisplayOrder` attribute值是10000.
参见以下示例:
````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; }
}
````
在这个示例中,inputs字段顺序为: `City` > `EmailAddress` > `PhoneNumber` > `Name` > `Surname`.
## 忽略属性
默认情况下, `abp-dynamic-form` 会为模型类中的每个属性生成输入. 如果要忽略属性请使用 `DynamicFormIgnore` attribute.
参见以下示例:
````csharp
public class MyModel
{
public string Name { get; set; }
[DynamicFormIgnore]
public string Surname { get; set; }
}
````
在这个示例中,不会为 `Surname` 属性生成输入.
## 指示文本框,单选按钮组和组合框
如果你已经阅读了[表单元素文档](Form-elements.md),你会注意到在c#模型上 `abp-radio``abp-select` 标签非常相. 我们必须使用 `[AbpRadioButton()]` attribute来告诉 `abp-dynamic-form` 你的哪些属性是单选按钮组,哪些属性是组合框.
参见以下示例:
````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
}
}
````
正如你上面的例子中看到:
* 如果在**Enum**属性上使用 `[AbpRadioButton()]`,它将是一个单选按钮组. 否则它是组合框.
* 如果在属性上使用 `[SelectItems()]``[AbpRadioButton()]`,那么它将是一个单选按钮组.
* 如果只在属性上使用 `[SelectItems()]`,它将是一个组合框.
* 如果一个属性没有使用这些属性,它将是一个文本框.
## 本地化
`abp-dynamic-form` 会处理本地化.
默认情况下, 它将尝试查找 "DisplayName:{PropertyName}" 或 "{PropertyName}" 定位本地化键,并将定位值设置为label.
你可以使用Asp.Net Core的 `[Display()]` attribute自行设置. 可以在此属性中使用本地化密钥. 请参阅以下示例:
````csharp
[Display(Name = "Name")]
public string Name { get; set; }
````

356
docs/zh-Hans/Unit-Of-Work.md

@ -1,3 +1,355 @@
## Unit of Work
# 工作单元
待添加
ABP框架的工作单元(UOW)实现提供了对应用程序中的**数据库连接和事务范围**的抽象和控制.
一旦一个新的UOW启动,它将创建一个**环境作用域**,当前作用域中执行的**所有数据库操作**都将参与该作用域并将其视为单个事务边界. 操作一起**提交**(成功时)或**回滚**(异常时).
ABP的UOW系统是;
* **按约定工作**, 所以大部分情况下你不需要处理UOW.
* **数据库提供者独立**.
* **Web独立**, 这意味着你可以在Web应用程序/服务之外的任何类型的应用程序中创建工作单元作用域.
## 约定
以下方法类型被认为是一个工作单元:
* ASP.NET Core MVC **Controller Actions**.
* ASP.NET Core Razor **Page Handlers**.
* **应用程序** 方法.
* **仓储方法**.
UOW自动针对这些方法开始,除非**周围已经有一个(环境)**UOW在运行.示例;
* 如果你调用一个[仓储]方法(Repositories.md),但还没有启动UOW,它将自动**启动一个新的事务UOW**,其中包括在仓储方法中完成的所有操作,如果仓储方法没有抛出任何异常,则**提交事务**. 仓储方法根本不知道UOW或事务. 它只在一个常规的数据库对象上工作(例如用于[EF Core](Entity-Framework-Core.md)的`DbContext`),而UOW由ABP框架处理.
* 如果调用[应用服务](Application-Services.md)方法,则相同的UOW系统将按上述说明工作. 如果应用服务方法使用某些仓储,这些仓储**不会开始新的UOW**,而是**参与由ABP框架为应用程序服务方法启动的当前工作单元中**.
* ASP.NET Core控制器操作也是如此. 如果操作以控制器action开始,**UOW范围是控制器action的方法主体**.
所有这些都是由ABP框架自动处理的.
### 数据库事务行为
虽然上一节解释了UOW是数据库事务,但实际上UOW不必是事务性的. 默认情况下;
* **HTTP GET**请求不会启动事务性UOW. 它们仍然启动UOW,但**不创建数据库事务**.
* 如果底层数据库提供程序支持数据库事务,那么所有其他HTTP请求类型都使用数据库事务启动UOW.
这是因为HTTP GET请求不会(也不应该)在数据库中进行任何更改. 你可以使用下面解释的选项来更改此行为.
## 默认选项
`AbpUnitOfWorkDefaultOptions` 用于配置工作单元系统的默认选项.在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中配置选项.
**示例: 完全禁用数据库事务**
````csharp
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});
````
### 选项属性
* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). 配置事务行为的全局点. 默认值为 `Auto` ,按照上面"*数据库事务行为"*一节的说明工作. 你可以使用此选项启用(甚至对于HTTP GET请求)或禁用事务.
* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值是 `null`** 并使用基础数据库提供程序的默认值.
* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务性的用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel).
## 控制工作单元
在某些情况下你可能希望更改常规事务作用域,创建内部作用域或精细控制事务行为. 下面几节将介绍这些可能性.
### IUnitOfWorkEnabled 接口
这是为不是按照上面解释的约定作为工作单元的类(或类的层次结构)启用UOW的一种简单方法.
**示例: 为任意服务实现 `IUnitOfWorkEnabled`**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency, IUnitOfWorkEnabled
{
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
}
}
````
然后 `MyService`(和它的派生类)方法都将是UOW.
但是为了使它工作,**有些规则应该被遵守**;
* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作).
* 仅异步方法(返回`Task`或`Task<T>`的方法)被拦截. 因此同步方法无法启动UOW.
> 注意,如果 `FooAsync` 在UOW作用域内被调用,那么它已经参与了UOW,不需要 `IUnitOfWorkEnabled` 或其他配置.
### UnitOfWorkAttribute
`UnitOfWork` attribute提供了更多的可能性,比如启用或禁用UOW和控制事务行为.
`UnitOfWork` attribute可以用于**类**或**方法**级别.
**示例: 为类的特定方法启用UOW**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
[UnitOfWork]
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
public virtual async Task BarAsync()
{
//this is a method without UOW
}
}
}
````
**示例: 为类的所有方法启用UOW**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
[UnitOfWork]
public class MyService : ITransientDependency
{
public virtual async Task FooAsync()
{
//this is a method with a UOW scope
}
public virtual async Task BarAsync()
{
//this is a method with a UOW scope
}
}
}
````
**同样的规则**也适用于此:
* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作).
* 仅异步方法(返回`Task`或`Task<T>`的方法)被拦截. 因此同步方法无法启动UOW.
#### UnitOfWorkAttribute 属性
* `IsTransactional` (`bool?`): 用于设置UOW是否是事务性的. **默认值为 `null`**. 如果你让它为 `null`,它会通过约定和配置自动确定.
* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值.
* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值.
* `IsDisabled` (`bool`): 用于禁用当前方法/类的UOW.
> 如果在环境UOW作用域内调用方法,将忽略 `UnitOfWork` 属性,并且该方法参与周围的事务.
**示例: 为控制器action禁用UOW**
````csharp
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Uow;
namespace AbpDemo.Web
{
public class MyController : AbpController
{
[UnitOfWork(IsDisabled = true)]
public virtual async Task FooAsync()
{
//...
}
}
}
````
## IUnitOfWorkManager
`IUnitOfWorkManager` 是用于控制工作单元系统的主要服务. 下面的部分解释了如何使用此服务(大多数时候你并不需要).
### 开始新的工作单元
`IUnitOfWorkManager.Begin` 方法用于创建一个新的UOW作用域.
**示例: 创建一个新的非事务性UOW作用域**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public virtual async Task FooAsync()
{
using (var uow = _unitOfWorkManager.Begin(
requiresNew: true, isTransactional: false
))
{
//...
await uow.CompleteAsync();
}
}
}
}
````
`Begin` 方法有以下可选参数:
* `requiresNew` (`bool`): 设置为 `true` 可忽略周围的工作单元,并使用提供的选项启动新的UOW. **默认值为`false`. 如果为`false`,并且周围有UOW,则 `Begin` 方法实际上不会开始新的UOW,而是以静默方式参与现有的UOW**.
* `isTransactional` (`bool`). 默认为 `false`.
* `isolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值.
* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值.
### 当前工作单元
如上所述UOW是环境的. 如果需要访问当前的工作单元,可以使用 `IUnitOfWorkManager.Current` 属性.
**示例: 获取当前UOW**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace AbpDemo
{
public class MyProductService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyProductService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public async Task FooAsync()
{
var uow = _unitOfWorkManager.Current;
//...
}
}
}
````
`Current` 属性返回一个 `IUnitOfWork` 对象.
> 如果没有周围的工作单元,则**当前工作单元可以为`null`**. 如上所述,如果你的类是常规的UOW类,你将其手动设置为UOW或在UOW作用域内调用它,那么该值就不会为 `null`.
#### SaveChangesAsync
`IUnitOfWork.SaveChangesAsync()` 方法将到目前为止的所有更改保存到数据库中. 如果你正在使用EF Core,它的行为完全相同. 如果当前UOW是事务性的,即使已保存的更改也可以在错误时回滚(对于支持的数据库提供程序).
**示例: 插入实体后保存更改以获取其自动增量ID**
````csharp
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace AbpDemo
{
public class CategoryAppService : ApplicationService, ICategoryAppService
{
private readonly IRepository<Category, int> _categoryRepository;
public CategoryAppService(IRepository<Category, int> categoryRepository)
{
_categoryRepository = categoryRepository;
}
public async Task<int> CreateAsync(string name)
{
var category = new Category {Name = name};
await _categoryRepository.InsertAsync(category);
//Saving changes to be able to get the auto increment id
await UnitOfWorkManager.Current.SaveChangesAsync();
return category.Id;
}
}
}
````
示例的 `Category` [实体](Entities.md)使用自动递增的 `int` 主键. 自动增量PK需要将实体保存到数据库中来获得新实体的ID.
示例是从基类 `ApplicationService` 派生的[应用服务](Application-Services.md), `IUnitOfWorkManager` 服务已经作为 `UnitOfWorkManager` 属性注入,所以无需手动注入.
获取当前UOW非常常见,所以还有一个 `UnitOfWorkManager.Current` 的快捷属性 `CurrentUnitOfWork`. 所以可以对上面的例子进行以下更改:
````csharp
await CurrentUnitOfWork.SaveChangesAsync();
````
##### SaveChanges() 的替代方法
由于经常需要在插入,更新或删除实体后保存更改,相应的[仓储](Repositories.md)方法有一个可选的 `autoSave` 参数. 可以将上面的 `CreateAsync` 方法按如下重写:
````csharp
public async Task<int> CreateAsync(string name)
{
var category = new Category {Name = name};
await _categoryRepository.InsertAsync(category, autoSave: true);
return category.Id;
}
````
如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`.
> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()` 和设置 `autoSave``true`.
>
> **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用.
#### IUnitOfWork 其他属性/方法
* `OnCompleted` 方法获得一个回调动作,当工作单元成功完成时调用(在这里你可以确保所有更改都保存了).
* `Failed``Disposed` 事件可以用于UOW失败和被销毁的通知.
* `Complete``Rollback` 方法用于完成(提交)或回滚当前 UOW, 通常ABP框架在内部使用,如果你使用 `IUnitOfWorkManager.Begin` 方法手动启动事务,那么你可以手动使用这些方法.
* `Options` 可用于获取启动UOW时使用的选项.
* `Items` 字典可用于在同一工作单元内存储和获取任意对象,可以实现自定义逻辑.
## ASP.NET Core 集成
工作单元系统已完全集成到ASP.NET Core. 它为UOW系统定义了动作过滤器和页面过滤器. 当你使用ASP.NET Core MVC控制器或Razor页面时,它可以正常工作.
> 使用ASP.NET Core时,通常你不需要做任何操作配置UOW.
### 工作单元中间件
`AbpUnitOfWorkMiddleware` 是可以在ASP.NET Core请求管道中启用UOW的中间件. 如果你需要扩大UOW范围以涵盖其他一些中间件,可以这样做.
**示例:**
````csharp
app.UseUnitOfWork();
app.UseConfiguredEndpoints();
````

172
docs/zh-Hans/Virtual-File-System.md

@ -2,109 +2,77 @@
虚拟文件系统使得管理物理上不存在于文件系统中(磁盘)的文件成为可能. 它主要用于将(js, css, image...)文件嵌入到程序集中, 并在运行时将它们象物理文件一样使用.
### Volo.Abp.VirtualFileSystem nuget包
## 安装
Volo.Abp.VirtualFileSystem是虚拟文件系统的核心包. 使用程序包管理器控制台(PMC)将其安装到项目中:
> 大多数情况下你不需要手动安装这个包,因为[应用程序启动模板](Startup-Templates/Application.md)已经预先安装.
```
Install-Package Volo.Abp.VirtualFileSystem
```
[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem) 是虚拟文件系统的NuGet主页.
> 启动模板默认已经安装了此nuget包, 所以在大多数情况下你不需要手动安装它.
使用ABP CLIi添加包到你的项目:
然后你可以在module中添加 **AbpVirtualFileSystemModule** 依赖项:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要添加 `Volo.Abp.VirtualFileSystem` 包的项目的 `.csproj` 文件目录打开命令行(终端).
* 运行 `abp add-package Volo.Abp.VirtualFileSystem` 命令.
```c#
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
如果你想要手动安装,安装[Volo.Abp.VirtualFileSystem](https://www.nuget.org/packages/Volo.Abp.VirtualFileSystem)NuGet包到你的项目并且添加`[DependsOn(typeof(AbpVirtualFileSystemModule))]`到你项目的[ABP Module](Module-Development-Basics.md)类.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
//...
}
}
```
## 与嵌入式文件工作
#### 注册嵌入文件
### 嵌入文件
要将文件嵌入到程序集中, 首先需要把该文件标记为嵌入式资源. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**.
要将文件嵌入到程序集中, 首先需要把该文件标记为**嵌入式资源**. 最简单的方式是在 **解决方案管理器** 中选择文件, 然后找到 **"属性"** 窗口将 **"生成操作"** 设置为 **"嵌入式资源"**.
例如:
![build-action-embedded-resource-sample](images/build-action-embedded-resource-sample.png)
如果需要添加多个文件, 这样做会很乏味. 作为选择, 你可以直接编辑 **.csproj** 文件:
````C#
<ItemGroup>
<EmbeddedResource Include="MyResources\**\*.*" />
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
````
如果文件名包含一些特殊字符,在项目/程序集中嵌入文件可能会导致问题. 为了克服这个限制;
1. 将[Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet包添加到包含嵌入式资源的项目中.
2. 添加 `<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>``.csproj` 文件的 `<PropertyConfig>...</PropertyConfig>` 部分中.
此配置以递归方式添加项目的 **MyResources** 文件夹下的所有文件(包括将来新添加的文件).
然后需要使用 `AbpVirtualFileSystemOptions` 来配置模块, 以便将嵌入式文件注册到虚拟文件系统. 例如:
> 尽管这两个步骤是可选的,并且ABP无需这些配置即可工作,但强烈建议你这样做.
````C#
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
### 配置AbpVirtualFileSystemOptions
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//Register all embedded files of this assembly to the virtual file system
options.FileSets.AddEmbedded<MyModule>("YourRootNameSpace");
});
使用 `AbpVirtualFileSystemOptions` [选项类](Options.md)可以在[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中将嵌入式文件注册到虚拟文件系统.
//...
}
}
}
````
`AddEmbedded` 扩展方法需要一个类, 从给定类的程序集中查找所有嵌入文件, 并将它们注册到虚拟文件系统. 它还有更简洁的写法:
**示例: 添加嵌入式文件到虚拟文件系统**
````C#
options.FileSets.Add(new EmbeddedFileSet(typeof(MyModule).Assembly), "YourRootNameSpace");
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
````
> "YourRootNameSpace" 是项目的根命名空间名字. 如果你的项目的根命名空间名字为空,则无需传递此参数.
`AddEmbedded` 扩展方法需要一个类, 从给定**类的程序集中查找所有嵌入文件**, 并将它们注册到虚拟文件系统. 通常将模块类作为通用参数传递.
#### 获取虚拟文件: IVirtualFileProvider
`AddEmbedded` 有两个可选参数;
将文件嵌入到程序集中并注册到虚拟文件系统后, 可以使用`IVirtualFileProvider`来获取文件或目录内容:
* `baseNamespace`: 仅在你没有配置上面说明的 `GenerateEmbeddedFilesManifest` 并且你的根名称空间不为空时,才可能需要这样做. 在这种情况下,请在此处设置你的根名称空间.
* `baseFolder`: 如果你不想公开项目中的所有嵌入式文件,只希望公开特定的文件夹(和子文件夹/文件),可以相对于项目根页面设置基本文件夹.
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
**示例: 添加项目中 `MyFiles` 目录下的文件**
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
````csharp
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>(
baseNamespace: "Acme.BookStore.MyFiles",
baseFolder: "/MyFiles"
);
});
````
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString(); //ReadAsString is an extension method of ABP
这个例子假设;
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider.GetDirectoryContents("/MyResources/js");
}
}
````
* 你的项目根(default)命令空间是 `Acme.BookStore`.
* 你的项目有一个名为 `MyFiles` 的目录.
* 你只想添加 `MyFiles` 目录到虚拟文件系统.
#### 在开发过程中处理嵌入式文件
@ -128,27 +96,61 @@ public class MyWebAppModule : AbpModule
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
//ReplaceEmbeddedByPhysical gets the root folder of the MyModule project
options.FileSets.ReplaceEmbeddedByPhysical<MyModule>(
Path.Combine(hostingEnvironment.ContentRootPath, "..\\MyModuleProject")
Path.Combine(
hostingEnvironment.ContentRootPath,
string.Format(
"..{0}MyModuleProject",
Path.DirectorySeparatorChar
)
)
);
});
}
//...
}
}
````
上面的代码假设`MyWebAppModule`和`MyModule`是Visual Studio解决方案中的两个不同的项目, `MyWebAppModule`依赖于`MyModule`.
> [应用程序启动模板]已经为本地化文件应用这个方法,所以当你更改一个本地化文件时,它会自动检测到更改.
## IVirtualFileProvider
将文件嵌入到程序集中并注册到虚拟文件系统后,可以使用 `IVirtualFileProvider` 接口来获取文件或目录内容:
````C#
public class MyService
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider)
{
_virtualFileProvider = virtualFileProvider;
}
public void Foo()
{
//Getting a single file
var file = _virtualFileProvider
.GetFileInfo("/MyResources/js/test.js");
var fileContent = file.ReadAsString();
//Getting all files/directories under a directory
var directoryContents = _virtualFileProvider
.GetDirectoryContents("/MyResources/js");
}
}
````
### ASP.NET Core 集成
虚拟文件系统与 ASP.NET Core 无缝集成:
* 虚拟文件可以像Web应用程序上的物理(静态)文件一样使用.
* Js, css, 图像文件和所有其他Web内容可以嵌入到程序集中并像物理文件一样使用.
* 应用程序(或其他模块)可以覆盖模块的虚拟文件, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样.
* 应用程序(或其他模块)可以**覆盖模块的虚拟文件**, 就像将具有相同名称和扩展名的文件放入虚拟文件的同一文件夹中一样.
#### 虚拟文件中间件
@ -160,4 +162,14 @@ app.UseVirtualFiles();
在静态文件中间件之后添加虚拟文件中间件, 使得通过在虚拟文件相同的位置放置物理文件, 从而用物理文件覆盖虚拟文件成为可能.
> 虚拟文件中间件可以虚拟wwwroot文件夹中的内容 - 就像静态文件一样.
> [应用程序启动模板](Startup-Templates/Application.md)已经配置了 `UseVirtualFiles()`.
#### 静态虚拟文件夹
默认情况下,ASP.NET Core仅允许 `wwwroot` 文件夹包含客户端使用的静态文件. 当你使用 `UseVirtualFiles` 中间件时以下文件夹也可以包含静态文件:
* Pages
* Views
* Themes
这允许你可以在 `.cshtml` 文件附近添加 `.js`, `.css`... 文件,更易于开发和维护你的项目.

22
docs/zh-Hans/docs-nav.json

@ -34,7 +34,7 @@
"path": "Tutorials/Part-1.md"
},
{
"text": "第章: 增删改查操作",
"text": "第章: 增删改查操作",
"path": "Tutorials/Part-2.md"
},
{
@ -154,16 +154,23 @@
]
},
{
"text": "事件",
"text": "事件总线",
"items": [
{
"text": "本地 Event Bus"
"text": "概述",
"path": "Event-Bus.md"
},
{
"text": "本地 Event Bus",
"path": "Local-Event-Bus.md"
},
{
"text": "分布式 Event Bus",
"path": "Distributed-Event-Bus.md",
"items": [
{
"text": "RabbitMQ 集成"
"text": "RabbitMQ 集成",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
}
]
}
@ -287,7 +294,8 @@
"path": "Data-Transfer-Objects.md"
},
{
"text": "工作单元"
"text": "工作单元",
"path": "Unit-Of-Work.md"
}
]
}
@ -360,6 +368,10 @@
{
"text": "Angular",
"items": [
{
"text": "v2.x 到 v3 迁移指南",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{
"text": "服务代理",
"path": "UI/Angular/Service-Proxies.md"

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

Loading…
Cancel
Save