Browse Source

Merge branch 'dev' into pr/10981

pull/10981/head
maliming 5 years ago
parent
commit
1b113fa3ad
No known key found for this signature in database GPG Key ID: 96224957E51C89E
  1. 16
      .github/workflows/auto-pr.yml
  2. 4
      README.md
  3. 64
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  4. 31
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  5. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json
  6. 26
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  7. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
  8. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json
  9. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json
  10. 9
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  11. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json
  12. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json
  13. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json
  14. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
  15. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json
  16. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json
  17. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json
  18. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json
  19. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json
  20. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json
  21. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json
  22. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json
  23. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json
  24. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json
  25. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json
  26. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json
  27. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
  28. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json
  29. 11
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  30. 4
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json
  31. 2
      build/common.ps1
  32. 4
      common.props
  33. 20
      delete-bin-obj-folders.bat
  34. 37
      delete-bin-obj-folders.js
  35. 16
      docs/en/API/API-Versioning.md
  36. 12
      docs/en/API/Dynamic-CSharp-API-Clients.md
  37. 217
      docs/en/API/Static-CSharp-API-Clients.md
  38. 24
      docs/en/API/Swagger-Integration.md
  39. 2
      docs/en/AspNet-Boilerplate-Migration-Guide.md
  40. 38
      docs/en/Autofac-Integration.md
  41. 17
      docs/en/Background-Jobs.md
  42. 4
      docs/en/Background-Workers-Hangfire.md
  43. 17
      docs/en/Background-Workers.md
  44. 2
      docs/en/Blog-Posts/2021-04-05 CmsKit/POST.md
  45. 196
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/POST.md
  46. BIN
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/cms-kit-global-resources.png
  47. BIN
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/libs-folder.png
  48. BIN
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/single-layer-solution.png
  49. BIN
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/suite-many-to-many-entity-relation.png
  50. BIN
      docs/en/Blog-Posts/2022-03-08 v5_2_Preview/video-single-layer-tutorial.png
  51. 51
      docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md
  52. 22
      docs/en/CLI.md
  53. 8
      docs/en/Caching.md
  54. 2
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/POST.md
  55. 2
      docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md
  56. 601
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md
  57. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif
  58. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png
  59. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png
  60. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif
  61. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif
  62. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png
  63. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png
  64. 529
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md
  65. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif
  66. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif
  67. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif
  68. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png
  69. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif
  70. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png
  71. 529
      docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md
  72. 548
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md
  73. BIN
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif
  74. BIN
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png
  75. 113
      docs/en/Community-Articles/2022-04-14-Dependency-Injection/POST.md
  76. 146
      docs/en/Concurrency-Check.md
  77. 59
      docs/en/Customizing-Application-Modules-Extending-Entities.md
  78. 2
      docs/en/Customizing-Application-Modules-Guide.md
  79. 70
      docs/en/Customizing-Application-Modules-Overriding-Services.md
  80. 98
      docs/en/Dependency-Injection.md
  81. 99
      docs/en/Deployment/Clustered-Environment.md
  82. 9
      docs/en/Deployment/Index.md
  83. 40
      docs/en/Distributed-Event-Bus.md
  84. 110
      docs/en/Distributed-Locking.md
  85. 4
      docs/en/Entity-Framework-Core-MySQL.md
  86. 2
      docs/en/Entity-Framework-Core-Oracle.md
  87. 23
      docs/en/Entity-Framework-Core-Other-DBMS.md
  88. 2
      docs/en/Entity-Framework-Core-PostgreSQL.md
  89. 2
      docs/en/Entity-Framework-Core-SQLite.md
  90. 4
      docs/en/Features.md
  91. 28
      docs/en/Getting-Started-AspNetCore-Application.md
  92. 6
      docs/en/Getting-Started-Create-Solution.md
  93. 4
      docs/en/Getting-Started-Running-Solution.md
  94. 9
      docs/en/Getting-Started-With-Startup-Templates.md
  95. 4
      docs/en/Migration-Guides/Abp-5-0-Blazor.md
  96. 2
      docs/en/Migration-Guides/Abp-5_0-Angular.md
  97. 2
      docs/en/Migration-Guides/Abp-5_0.md
  98. 52
      docs/en/Migration-Guides/Abp-5_2.md
  99. 3
      docs/en/Migration-Guides/Index.md
  100. 43
      docs/en/Module-Development-Basics.md

16
.github/workflows/auto-pr.yml

@ -1,10 +1,10 @@
name: Merge branch dev with rel-5.1
name: Merge branch dev with rel-5.3
on:
push:
branches:
- rel-5.1
- rel-5.3
jobs:
merge-dev-with-rel-5-1:
merge-rel-5-3-with-rel-5-2:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -12,13 +12,13 @@ jobs:
ref: dev
- name: Reset promotion branch
run: |
git fetch origin rel-5.1:rel-5.1
git reset --hard rel-5.1
git fetch origin rel-5.3:rel-5.3
git reset --hard rel-5.3
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
branch: auto-merge/rel-5-1/${{github.run_number}}
title: Merge branch dev with rel-5.1
body: This PR generated automatically to merge dev with rel-5.1. Please review the changed files before merging to prevent any errors that may occur.
branch: auto-merge/rel-5-3/${{github.run_number}}
title: Merge branch dev with rel-5.3
body: This PR generated automatically to merge dev with rel-5.3. Please review the changed files before merging to prevent any errors that may occur.
reviewers: ${{github.actor}}
token: ${{ github.token }}

4
README.md

@ -108,6 +108,10 @@ ABP is a community-driven open source project. See [the contribution guide](http
Love ABP Framework? **Please give a star** to this repository :star:
## Discord Channel
You can use this link to join the ABP Community Discord Server: https://discord.gg/abp
## ABP Commercial
See also [ABP Commercial](https://commercial.abp.io/) if you are looking for pre-built application modules, professional themes, code generation tooling and premium support for the ABP Framework.

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

@ -306,10 +306,10 @@
"PaymentTimeCannotBeFutureTime": "Payment time cannot be a future time!",
"SaveAndDownload": "Save and Download",
"BillingInfo": "Billing Info",
"DeleteInvoice" : "Delete Invoice",
"PaymentStateSetTo" : "Payment state set to {0}",
"DeleteInvoice": "Delete Invoice",
"PaymentStateSetTo": "Payment state set to {0}",
"ChangeState": "Change State",
"Permission:TrialLicense" : "Trial License",
"Permission:TrialLicense": "Trial License",
"Menu:TrialLicenses": "Trial Licenses",
"TrialLicenses": "Trial Licenses",
"UserNameFilter": "Username",
@ -337,18 +337,6 @@
"Expired": "Expired",
"TrialLicenseDeletionWarningMessage": "Are you sure you want to delete the trial license? Trial license, organization, support accounts will be deleted!",
"LicenseCategoryFilter": "License category",
"Volo.AbpIo.Commercial:030000": "You already used your trial period.",
"Volo.AbpIo.Commercial:030001": "This organization name already exists.",
"Volo.AbpIo.Commercial:030002": "Once activated, trial license cannot be set to requested!",
"Volo.AbpIo.Commercial:030003": "There is no such status!",
"Volo.AbpIo.Commercial:030004": "Status could not be changed due to an unexpected error!",
"Volo.AbpIo.Commercial:030005": "Start and end date can be updated when the trial license is in the -activated- status!",
"Volo.AbpIo.Commercial:030006": "End date must always be greater than start date!",
"Volo.AbpIo.Commercial:030007": "This trial license has already been activated once!",
"Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!",
"Volo.AbpIo.Commercial:030009": "User not found!",
"Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!",
"Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!",
"Permission:SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmailWarningMessage": "Are you sure you want to send welcome email to the organization members?",
@ -359,12 +347,52 @@
"PurchaseDate": "Purchase date",
"IsAbpBookDownloaded": "ABP book downloaded",
"IsMasteringAbpBookDownloadEnabled": "ABP Book download enabled",
"Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link",
"Permission:Accounting:CustomPaymentLinkGenerator": "Custom Payment Link",
"CustomPaymentLink": "Custom Payment Link",
"Menu:CustomPaymentLink": "Custom Payment Link",
"Amount": "Amount",
"GenerateCustomPaymentLink": "Generate Custom Payment Link",
"GeneratedPaymentLink": "Generated Payment Link",
"CopyText": "Copy Text"
"CopyText": "Copy Text",
"Permission:CommunityEvents": "Events",
"Menu:Events": "Events",
"Events": "Events",
"EventType": "Event Type",
"Number": "Number",
"RegistrationURL": "Registration URL",
"URL": "URL",
"EventDeletionConfirmationMessage": "Are you sure you want to delete this event?",
"Enum:EventType:0": "Community Talks",
"CreateAnEvent": "Create an event",
"Permission:CommunitySpeakers": "Speakers",
"CreateASpeaker": "Create a speaker",
"Speakers": "Speakers",
"Image": "Image",
"GithubURL": "Github URL",
"SpeakerDeletionConfirmationMessage": "Are you sure you want to delete this speaker?",
"Menu:Speakers": "Speakers",
"ChooseSpeakerImage": "Choose a speaker image...",
"SpeakerImage": "Speaker image",
"AddSpeaker": "Add Speaker",
"ShowPurchaseItemsOfOrganizations": "Purchase Items",
"Enum:OrganizationPurchaseState:0": "Not delivered",
"Enum:OrganizationPurchaseState:1": "Delivered",
"PurchaseItems": "Purchase Items",
"SuccessfullyUpdated": "Successfully updated",
"SuccessfullyAdded": "Successfully added",
"PurchaseState": "Purchase State",
"ShowBetweenDayCount": "Show Between Days",
"PurchaseOrder": "Purchase Order",
"ShowCreateInvoiceOfOrganization": "Create Invoice",
"ShowCreateQuotationOfOrganization": "Create Quotation",
"BookDiscounts": "Book Discounts",
"Permission:BookDiscount": "Book Discount",
"Menu:BookDiscounts": "Book Discounts",
"BookType": "Book Type",
"PurchasePlatform": "Purchase Platform",
"StartTime": "Start Time",
"EndTime": "End Time",
"CreateABookDiscount": "Create a book discount",
"BookDiscountDeletionConfirmationMessage": "Are you sure you want to delete this book discount?"
}
}
}

31
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -14,6 +14,18 @@
"Volo.AbpIo.Domain:020002": "Could not delete this NPM Package because \"{Modules}\" Modules are using this package.",
"Volo.AbpIo.Domain:020003": "Could not delete this NPM Package because \"{Modules}\" Modules are using this package and \"{NugetPackages}\" Nuget Packages are dependent to this package.",
"Volo.AbpIo.Domain:020004": "Could not delete this Nuget Package because \"{Modules}\" Modules are using this package.",
"Volo.AbpIo.Domain:030000": "You have already completed your trial period.",
"Volo.AbpIo.Domain:030001": "This organization name already exists.",
"Volo.AbpIo.Domain:030002": "Once activated, you cannot switch the trial license to -requested- status!",
"Volo.AbpIo.Domain:030003": "There is no such status!",
"Volo.AbpIo.Domain:030004": "Status could not be changed due to an unexpected error!",
"Volo.AbpIo.Domain:030005": "Start and end date can be updated when the trial license is in the -activated- status!",
"Volo.AbpIo.Domain:030006": "The end date must be greater than the start date!",
"Volo.AbpIo.Domain:030007": "This trial license has already been activated!",
"Volo.AbpIo.Domain:030008": "The purchase date can be set only when the status is -purchased-!",
"Volo.AbpIo.Domain:030009": "User not found!",
"Volo.AbpIo.Domain:030010": "To purchase the trial license, you first need to activate your trial license!",
"Volo.AbpIo.Domain:030011": "You cannot delete a trial license when it is purchased!",
"WantToLearn?": "Want to learn?",
"ReadyToGetStarted?": "Ready to get started?",
"JoinOurCommunity": "Join our community",
@ -72,7 +84,7 @@
"WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials like product deals & special offers.",
"JoinOurMarketingNewsletter": "Join our marketing newsletter",
"CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"ABPIO-Common": "ABPIO-Common",
"WouldLikeToReceiveNotification": "I would like to receive the latest news from abp.io websites.",
"CommercialNewsletterConfirmationMessage": "I agree to the <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"FreeDDDEBook": "Free DDD E-Book",
"AdditionalServices": "Additional Services",
@ -98,6 +110,21 @@
"EULA": "EULA",
"ABPCommercialIntroductionMessage": "Pre-built application modules, advanced startup templates, rapid application development tooling, professional UI themes and premium support.",
"MasteringAbpFrameworkEBook": "Mastering ABP Framework",
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques."
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques.",
"Speakers": "Speakers",
"PreviousEvents": "Previous Events",
"WatchTheEvent": "Watch the Event",
"RegisterNow": "Register Now",
"ThereIsNoEvent": "There is no event.",
"Events": "Events",
"Volo.AbpIo.Domain:080000": "There is already a purchase item named \"{Name}\"",
"MasteringAbpFrameworkBook": "Book: Mastering ABP Framework",
"ABPIO-CommonPreferenceDefinition": "Get the latest news about ABP Platform like new posts, events and more.",
"BuiltOn": "Built-on",
"AbpFramework": "ABP Framework",
"Volo.AbpIo.Domain:080001": "Start Time can not be greater than End Time",
"Enum:BookType:0": "Mastering ABP Framework",
"Enum:PurchasePlatform:0": "Amazon",
"Enum:PurchasePlatform:1": "Packt"
}
}

3
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json

@ -108,6 +108,7 @@
"ExtendNow": "Extend Now",
"ContinueWithNewOrganization": "Continue with a new organization",
"RenewLicenseEarly": "If I renew my license early, will I get the full year?",
"RenewLicenseEarylExplanation": "When you renew your license before your license expiry date, 1 year will be added to your license expiry date. For example, if your license expires on {0}-06-06 and you renew it on {0}-01-01, your new license expiry date will be {1}-06-06."
"RenewLicenseEarylExplanation": "When you renew your license before your license expiry date, 1 year will be added to your license expiry date. For example, if your license expires on {0}-06-06 and you renew it on {0}-01-01, your new license expiry date will be {1}-06-06.",
"LicenseTypeNotCorrect": "The license type is not correct!"
}
}

26
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json

@ -49,7 +49,7 @@
"IndexPageHeroSection": "<span class=\"first-line\">A complete</span><span class=\"second-line\"> web development platform</span><span class=\"third-line\">built-on <img src=\"{0}\" width=\"110\" class=\"ml-1\" /> framework</span>",
"AbpCommercialShortDescription": "ABP Commercial provides pre-built application modules, rapid application development tooling, professional UI themes, premium support and more.",
"LiveDemo": "Live Demo",
"GetLicence": "Get a Licence",
"GetLicence": "Get a License",
"Application": "Application",
"StartupTemplates": "Startup Templates",
"Startup": "Startup",
@ -199,12 +199,13 @@
"WhatHappensWhenLicenseEndsExplanation5": "You can not use the ABP Suite.",
"WhatHappensWhenLicenseEndsExplanation6": "You can not get the <a href=\"{0}\">premium support</a> anymore.",
"WhatHappensWhenLicenseEndsExplanation7": "You can extend (renew) your license if you want to continue getting these benefits. If you extend your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount.",
"WhatHappensWhenLicenseEndsExplanation8": "The ABP projects you generated are not stored on our servers. Therefore, it is your responsibility to keep the source code you download. When your license expires, there's no way to get your generated ABP project source code.",
"WhenShouldIRenewMyLicense": "When should I renew my license?",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount. However, if you renew your license after <strong>1 month</strong> since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"TrialPlan": "Do you have a trial plan?",
"TrialPlanExplanation": "For now, ABP Commercial doesn't have a trial plan. For the Team licenses we provide 30 days money back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br />After sending the license fee via bank transfer, email us your receipt and the type of license requested at accounting@abp.io, here's our international bank account information:",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br/>After sending the license fee via bank transfer, send your receipt and requested license type to accounting@abp.io.<br/>Our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",
"HowToUpgradeExplanation1": "When you create a new application using ABP Commercial, all the modules and theme are used as NuGet and NPM packages. So, you can easily upgrade the packages when a new version is available.",
"HowToUpgradeExplanation2": "In addition to the standard NuGet/NPM upgrades, <a href=\"{0}\">ABP CLI</a> provides an update command that automatically finds and upgrades all ABP related packages in your solution.",
@ -271,7 +272,7 @@
"Enterprise": "Enterprise",
"Custom": "Custom",
"IncludedDeveloperLicenses": "Included developer licenses",
"CustomLicenceOrAdditionalServices": "Need custom licence or additional services?",
"CustomLicenceOrAdditionalServices": "Need custom license or additional services?",
"CustomOrVolumeLicense": "Custom or volume license",
"LiveTrainingSupport": "Live training & support",
"AndMore": "and more",
@ -490,6 +491,23 @@
"ABPFrameworkEBook": "E-Book: Mastering ABP Framework",
"MasteringAbpFrameworkEBookDescription": "Included within your ABP Commercial license",
"Volo.AbpIo.Domain:070000": "The organization name can only contain latin letters, numbers, dots and hyphens!",
"Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!"
"Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!",
"FullName": "Full Name",
"LicenseTypeNotCorrect": "The license type is not correct!",
"Trainings": "Trainings",
"ChoseTrainingPlaceholder": "Chose the training...",
"DoYouNeedTrainings": "Do you need one of these trainings?",
"DoYouNeedTraining": "Do you need {0} training?",
"GetInTouchUs": "Get in touch with us",
"ForMoreInformationClickHere": "For more information, click <a href='{0}'>here.</a>",
"IsGetOnboardingTraining": "Would you like to get onboarding & web application development training?",
"OnboardingWebApplicationDevelopmentTrainingMessage": "To schedule your training calendar, please contact {0} after creating the organization",
"Note": "Note",
"AdditionalNote": "Additional Note",
"OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?",
"OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at <a href=\"mailto:info@abp.io\">info@abp.io</a>.",
"AddBasket": "Add to Basket",
"SendTrainingRequest": "Send Training Request",
"OnlyEnglishVersionOfThisDocumentIsTheRecentAndValid": "* The English version of this document is the most up-to-date and the English version will prevail in any dispute."
}
}

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "لإنشاء منشور فعال ، أضف صورة غلاف. قم بتحميل صور بنسبة عرض إلى ارتفاع تبلغ 16: 9 للحصول على أفضل عرض. الحد الأقصى لحجم الملف: 1 ميغا بايت.",
"ThankYouForContribution": "شكرًا لك على المساهمة في مجتمع ABP.",
"GithubPost": "المادة جيثب",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> مثال </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> مثال </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> أرسل عنوان URL لمقالتك باستخدام النموذج.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> سيتم عرض مقالتك في موقع الويب هذا.",
"YoutubeVideo": "فيديو يوتيوب",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pro vytvoření efektního příspěvku přidejte titulní fotku. Pro nejlepší zobrazení nahrajte obrázky s poměrem stran 16:9. Maximální velikost souboru: 1 MB.",
"ThankYouForContribution": "Děkujeme vám za příspěvek do komunity ABP.",
"GithubPost": "Článek Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">příklad </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">příklad </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Odešlete adresu URL svého článku pomocí formuláře.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Váš článek bude vykreslen na této webové stránce.",
"YoutubeVideo": "Youtube Video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Um einen effektiven Beitrag zu erstellen, fügen Sie ein Titelbild hinzu. Laden Sie Bilder mit einem Seitenverhältnis von 16:9 hoch, um die beste Ansicht zu erhalten. Maximale Dateigröße: 1 MB.",
"ThankYouForContribution": "Vielen Dank für Ihren Beitrag zur ABP-Community.",
"GithubPost": "Github-Artikel",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">Beispiel </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">Beispiel </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Senden Sie Ihre Artikel-URL über das Formular.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Ihr Artikel wird auf dieser Website wiedergegeben.",
"YoutubeVideo": "Youtube-Video",

9
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -104,7 +104,7 @@
"VideoPost": "Video Post",
"Post": "Post",
"Read": "Read",
"CreateGithubPostUrlInfo": "Original GitHub URL of the post.",
"CreateGithubPostUrlInfo": "Full URL of the Markdown file on GitHub (<a href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\" target=\"blank\">example</a>).",
"CreateVideoContentUrlInfo": "Original Youtube URL of the post.",
"CreateExternalPostUrlInfo": "Original External Url of the post.",
"VideoContentForm": "Submit Video on YouTube",
@ -117,7 +117,7 @@
"ExternalPostUrl": "External Post Url",
"ThankYouForContribution": "Thank you for contributing to the ABP Community.",
"GithubPost": "GitHub Post",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Write a post on any public GitHub repository with the Markdown format. <a target=\"_blank\" class=\"fw-6 text-white text-decoration-underline\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">example</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Write a post on any public GitHub repository with the Markdown format. <a target=\"_blank\" class=\"fw-6 text-white text-decoration-underline\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">example</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Submit your post URL using the form.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Your post will be rendered in this website.",
"YoutubeVideo": "Youtube Video",
@ -162,6 +162,9 @@
"Previous": "Previous",
"Next": "Next",
"Share": "Share",
"SortBy": "Sort by"
"SortBy": "Sort by",
"NoPublishedEventsYet": "No published events yet.",
"SubscribeYoutubeChannel": "Subscribe to the Youtube Channel",
"Enum:EventType:0": "Talks"
}
}

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Para crear una publicación eficaz, agregue una foto de portada. Cargue imágenes con una relación de aspecto de 16: 9 para obtener la mejor vista. Tamaño máximo de archivo: 1 MB.",
"ThankYouForContribution": "Gracias por contribuir a la comunidad ABP.",
"GithubPost": "Artículo de Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> ejemplo </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> ejemplo </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Envíe la URL de su artículo mediante el formulario.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Su artículo se mostrará en este sitio web.",
"YoutubeVideo": "Video de Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Lisää kansikuva, jotta voit luoda tehokkaan viestin. Lataa 16:9 -kuvasuhteen kuvat parhaan näkymän saamiseksi. Tiedoston enimmäiskoko: 1MB.",
"ThankYouForContribution": "Kiitos osallistumisesta ABP Community.",
"GithubPost": "Github-artikkeli",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> esimerkki </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> esimerkki </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2.</span> Lähetä artikkelin URL-osoite lomaketta käyttämällä.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3.</span> Artikkelisi renderöidään tällä verkkosivustolla.",
"YoutubeVideo": "Youtube-video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pour créer un post efficace, ajoutez une photo de couverture. Téléchargez des images au format 16: 9 pour une meilleure vue. Taille maximale du fichier: 1 Mo.",
"ThankYouForContribution": "Merci de contribuer à la communauté ABP.",
"GithubPost": "Post Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1.</span> Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"></a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1.</span> Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"></a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2.</span> Envoyez l'URL de votre post à l'aide du formulaire.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3.</span> Votre post sera rendu sur ce site Web.",
"YoutubeVideo": "Vidéo Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "एक प्रभावी पोस्ट बनाने के लिए, एक कवर फ़ोटो जोड़ें। सर्वश्रेष्ठ दृश्य के लिए 16: 9 पहलू अनुपात चित्र अपलोड करें। अधिकतम फ़ाइल आकार: 1 एमबी।",
"ThankYouForContribution": "ABP समुदाय में योगदान के लिए धन्यवाद।",
"GithubPost": "गीथब लेख",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">उदाहरण </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">उदाहरण </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2। </span> फ़ॉर्म का उपयोग करके अपना लेख URL सबमिट करें।",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3। </span> इस वेब साइट में आपका लेख प्रस्तुत किया जाएगा।",
"YoutubeVideo": "यूट्यूब वीडियो",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Hatékony bejegyzés létrehozásához adj hozzá egy borítóképet. Tölts fel 16:9-es képarányú képeket a legjobb nézet érdekében. Maximális fájlméret: 1 MB.",
"ThankYouForContribution": "Köszönjük, hogy hozzájárult az ABP közösséghez.",
"GithubPost": "Github cikk",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">példa </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">példa </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Az űrlap segítségével küldje el cikkének URL-jét.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> A cikk ezen a webhelyen jelenik meg.",
"YoutubeVideo": "Youtube videó",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Til að búa til áhrifaríka færslu skaltu bæta við forsíðumynd. Hladdu upp 16: 9 myndhlutföllum fyrir besta útsýnið. Hámarks skráarstærð: 1MB.",
"ThankYouForContribution": "Þakka þér fyrir að leggja þitt af mörkum til ABP samfélagsins.",
"GithubPost": "Github Greinar",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold \"> 1. </span> Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. <a target=\"_ blank \" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts 2020-12-04-Event-Organizer/Post.md \"> dæmi </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold \"> 1. </span> Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. <a target=\"_ blank \" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles 2020-12-04-Event-Organizer/Post.md \"> dæmi </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Sendu vefslóð greinarinnar með því að nota formið.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Grein þín verður birt á þessari vefsíðu.",
"YoutubeVideo": "Youtube Videó",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Per creare un post efficace, aggiungi una foto di copertina. Carica immagini in formato 16: 9 per una visualizzazione migliore. Dimensione massima del file: 1MB.",
"ThankYouForContribution": "Grazie per aver contribuito alla comunità ABP.",
"GithubPost": "Articolo GitHub",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">esempio</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">esempio</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Invia l'URL del tuo articolo utilizzando il modulo.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Il tuo articolo verrà visualizzato in questo sito web.",
"YoutubeVideo": "Video Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Voeg een omslagfoto toe om een effectieve post te maken. Upload afbeeldingen met een beeldverhouding van 16:9 voor de beste weergave. Maximale bestandsgrootte: 1 MB.",
"ThankYouForContribution": "Bedankt voor uw bijdrage aan de ABP-community.",
"GithubPost": "Github-artikel",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">voorbeeld </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">voorbeeld </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Dien uw artikel-URL in via het formulier.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Uw artikel wordt weergegeven op deze website.",
"YoutubeVideo": "Youtube video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Aby stworzyć efektowny post, dodaj zdjęcie na okładkę. Prześlij zdjęcia o proporcjach 16:9, aby uzyskać najlepszy widok. Maksymalny rozmiar pliku: 1 MB.",
"ThankYouForContribution": "Dziękujemy za udział w społeczności ABP.",
"GithubPost": "Artykuł na Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">przykład </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">przykład </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Prześlij adres URL artykułu za pomocą formularza.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Twój artykuł zostanie wyrenderowany w tej witrynie internetowej.",
"YoutubeVideo": "Wideo z YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Para criar uma postagem eficaz, adicione uma foto de capa. Carregue imagens de proporção de 16: 9 para a melhor visualização. Tamanho máximo do arquivo: 1 MB.",
"ThankYouForContribution": "Obrigado por contribuir com a Comunidade ABP.",
"GithubPost": "Artigo Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> exemplo </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> exemplo </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Envie o URL do seu artigo usando o formulário.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Seu artigo será processado neste site.",
"YoutubeVideo": "Vídeo do youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pentru crearea unei postări eficace, adăugaţi o poză de copertă. Încărcaţi imagini cu raport de aspect 16:9 pentru cea mai bună vizualizare. Dimensiunea maximă a fişierului: 1MB.",
"ThankYouForContribution": "Vă mulţumim pentru că aţi contribuit la comunitatea ABP.",
"GithubPost": "Articol GitHub",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrieţi un articol pe orice repository GitHub folosind formatul Markup. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">exemplu</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrieţi un articol pe orice repository GitHub folosind formatul Markup. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">exemplu</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Trimiteţi Url-ul articolului folosind formularul.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Articolul dumneavoastră va fi redat în acest site web.",
"YoutubeVideo": "Videoclip YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Для создания эффектного поста добавьте фото обложки. Загрузите изображения с соотношением сторон 16: 9 для наилучшего просмотра. Максимальный размер файла: 1 МБ.",
"ThankYouForContribution": "Спасибо за ваш вклад в сообщество ABP.",
"GithubPost": "Статья на Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Отправьте URL своей статьи, используя форму.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Ваша статья будет отображаться на этом веб-сайте.",
"YoutubeVideo": "YouTube видео",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Ak chcete vytvoriť efektívny príspevok, pridajte titulnú fotografiu. Pre optimálne zobrazenie nahrávajte obrázky s pomerom strán 16:9. Maximálna veľkosť súboru: 1 MB.",
"ThankYouForContribution": "Ďakujeme, že ste prispeli do ABP komunity.",
"GithubPost": "Článok na Githube",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">príklad</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">príklad</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Odoslať URL adresu článku pomocou formulára.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Váš článok sa zobrazí na tejto webovej stránke.",
"YoutubeVideo": "Video na YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Če želite ustvariti učinkovito objavo, dodajte naslovno fotografijo. Naložite slike v razmerju stranic 16:9 za najboljši pogled. Največja velikost datoteke: 1 MB.",
"ThankYouForContribution": "Hvala za prispevek k skupnosti ABP.",
"GithubPost": "Članek Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">primer </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">primer </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Pošljite URL svojega članka z uporabo obrazca.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Vaš članek bo prikazan na tem spletnem mestu.",
"YoutubeVideo": "Youtube video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Etkili bir gönderi oluşturmak için bir kapak fotoğrafı ekleyin. En iyi görünüm için 16:9 en boy oranlı resimler yükleyin. Maksimum dosya boyutu: 1MB.",
"ThankYouForContribution": "ABP Topluluğuna katkıda bulunduğunuz için teşekkür ederiz.",
"GithubPost": "Github Makalesi",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">örnek </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">örnek </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Formu kullanarak makale URL'nizi gönderin.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Makaleniz bu web sitesinde işlenecektir.",
"YoutubeVideo": "Youtube videosu",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Để tạo một bài đăng hiệu quả, hãy thêm ảnh bìa. Tải lên hình ảnh tỷ lệ khung hình 16: 9 để có chế độ xem tốt nhất. Kích thước tệp tối đa: 1MB.",
"ThankYouForContribution": "Cảm ơn bạn đã đóng góp cho Cộng đồng ABP.",
"GithubPost": "Bài báo trên Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Viết một bài báo trên bất kỳ kho lưu trữ GitHub công khai nào với định dạng Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> ví dụ </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Viết một bài báo trên bất kỳ kho lưu trữ GitHub công khai nào với định dạng Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> ví dụ </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Gửi URL bài viết của bạn bằng biểu mẫu.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Bài viết của bạn sẽ được hiển thị trên trang web này.",
"YoutubeVideo": "Video trên Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要创建一个有效的帖子,需要添加封面图片. 上传16:9的图片获取最佳视觉效果. 文件最大支持: 1MB",
"ThankYouForContribution": "感谢你对ABP社区的贡献",
"GithubPost": "Github文章",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用Markdown格式在GitHub的任何公共存储库上写一篇文章. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用Markdown格式在GitHub的任何公共存储库上写一篇文章. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表单提交你的文章URL.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 你的文章将在这个网站中呈现.",
"YoutubeVideo": "Youtube视频",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要創建有效的帖子,請添加封面照片。上傳 16:9 縱橫比的圖片以獲得最佳觀看效果。最大文件大小:1MB。",
"ThankYouForContribution": "感謝您為 ABP 社區做出貢獻。",
"GithubPost": "Github 文章",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表單提交您的文章 URL。",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 您的文章將在此網站中呈現。",
"YoutubeVideo": "優酷視頻",

11
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -154,7 +154,7 @@
"AndMore": "and more...",
"Code": "Code",
"Result": "Result",
"SeeTheDocumentForMoreInformation": "Check out the <a href=\"{1}\">{0} document</a> for more information",
"SeeTheDocumentForMoreInformation": "Check out the <a target=\"_blank\" 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": "Email address",
@ -183,7 +183,7 @@
"InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:",
"DifferentLevelOfNamespaces": "You can use different levels of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.",
"ABPCLIExamplesInfo": "The <strong>new</strong> command creates a <strong>layered MVC application</strong> with <strong>Entity Framework Core</strong> as the database provider. However, it has additional options.",
"SeeCliDocumentForMoreInformation": "Check out the <a href=\"{0}\">ABP CLI document</a> for more options or select the \"Direct Download\" tab above.",
"SeeCliDocumentForMoreInformation": "Check out the <a target=\"_blank\" href=\"{0}\">ABP CLI document</a> for more options or select the \"Direct Download\" tab above.",
"Optional": "Optional",
"LocalFrameworkRef": "Keep the local project reference for the framework packages.",
"BlobStoring": "BLOB Storing",
@ -214,7 +214,11 @@
"SeeDocs": "See Docs",
"None": "None",
"Application": "Application",
"ApplicationExplanation": "Creates a fully layered solution based on Domain Driven Design practices. Recommended for long-term projects that need a maintainable and extensible codebase.",
"ApplicationNoLayer": "Application (single layer)",
"ApplicationNoLayerExplanation": "Creates a single-layer web application. Recommended for building an application with a simpler and easy to understand architecture.",
"Module": "Module",
"ModuleExplanation": "Creates a reusable, fully layered application module solution. You can use this option to create modules for your modular application.",
"PackageName": "Package Name",
"LicenseURL": "License URL",
"License": "License",
@ -318,6 +322,7 @@
"InstallingTheABPCLI": "Installing the ABP CLI",
"CreateYourProjectNow": "Create Your Project Now",
"OrderOn": "Order on {0}",
"DownloadFreeDDDBook": "Download Free DDD Book"
"DownloadFreeDDDBook": "Download Free DDD Book",
"WhatIsABPFramework": "What is the ABP Framework?"
}
}

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

@ -214,7 +214,11 @@
"SeeDocs": "Dökümanı Görüntüle",
"None": "Hiç",
"Application": "Uygulama",
"ApplicationExplanation": "Domain Driven Design pratikleri üzerine oluşturulmuş çok katmanlı bir çözüm oluşturur. Bakıma ve geliştirmeye açık kod tabanına ihtiyaç duyan uzun süreli projeler için önerilir.",
"ApplicationNoLayer": "Uygulama (tek katmanlı)",
"ApplicationNoLayerExplanation": "Tek katmanlı Web uygulaması oluşturur. Daha basit ve anlaması kolay mimari ile uygulama geliştirmek için önerilir.",
"Module": "Modül",
"ModuleExplanation": "Tamamen katmanlanmış, tekrar kullanılabilir bir uygulama modülü oluşturur. Uygulamanıza modül yaratmak için bu seçeneği kullanabilirsiniz.",
"PackageName": "Paket adı",
"LicenseURL": "Lisans Linki",
"License": "Lisans",

2
build/common.ps1

@ -7,7 +7,6 @@ $rootFolder = (Get-Item -Path "./" -Verbose).FullName
# List of solutions used only in development mode
$solutionPaths = @(
"../framework",
"../studio",
"../modules/basic-theme",
"../modules/users",
"../modules/permission-management",
@ -15,6 +14,7 @@ $solutionPaths = @(
"../modules/feature-management",
"../modules/identity",
"../modules/identityserver",
"../modules/openiddict",
"../modules/tenant-management",
"../modules/audit-logging",
"../modules/background-jobs",

4
common.props

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>5.2.0</Version>
<Version>5.3.0-rc.1</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>
@ -28,4 +28,4 @@
<EmbeddedResource Include="**\*generate-proxy.json" />
<Content Remove="**\*generate-proxy.json" />
</ItemGroup>
</Project>
</Project>

20
delete-bin-obj-folders.bat

@ -1,20 +0,0 @@
@ECHO off
cls
ECHO Deleting all BIN and OBJ folders...
ECHO.
FOR /d /r . %%d in (bin,obj) DO (
IF EXIST "%%d" (
ECHO %%d | FIND /I "\node_modules\" > Nul && (
ECHO.Skipping: %%d
) || (
ECHO.Deleting: %%d
rd /s/q "%%d"
)
)
)
ECHO.
ECHO.BIN and OBJ folders have been successfully deleted. Press any key to exit.
pause > nul

37
delete-bin-obj-folders.js

@ -1,37 +0,0 @@
const { resolve } = require("path");
const { readdir, rmdir } = require("fs").promises;
async function* removeFolder(dir) {
await rmdir(dir, { recursive: true });
yield dir;
}
async function* getBinObj(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
for (const dirent of dirents) {
if (!dirent.isDirectory()) continue;
const name = dirent.name;
if (name === "node_modules") continue;
const res = resolve(dir, dirent.name);
if (name === "bin" || name === "obj") {
yield* removeFolder(res);
continue;
}
yield* getBinObj(res);
}
}
(async () => {
console.log("\x1b[36m%s\x1b[0m", "Deleting all BIN and OBJ folders...");
for await (const dir of getBinObj(".")) {
console.log("\x1b[33m%s\x1b[0m", `Removed: ${dir}`);
}
console.log("\x1b[36m%s\x1b[0m", "All BIN and OBJ folders are deleted.");
})();

16
docs/en/API/API-Versioning.md

@ -26,7 +26,7 @@ public override void ConfigureServices(ServiceConfigurationContext context)
## C# and JavaScript Static Client Proxies
This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), I suggest you always use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String).
This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), we suggest to use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String).
### Example
@ -45,7 +45,7 @@ public interface IBookV2AppService : IApplicationService
}
```
**HttpApi Controillers:**
**HttpApi Controllers:**
```cs
[Area(BookStoreRemoteServiceConsts.ModuleName)]
[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)]
@ -175,7 +175,7 @@ bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams)
```
## Manually change version
## Changing version manually
If an application service class supports multiple versions. You can inject `ICurrentApiVersionInfo` to switch versions in C#.
@ -337,13 +337,15 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
}
```
## Custom multi-version API controller.
## Custom multi-version API controller
ABP Framework will not affect to your APIs, you can freely implement your APIs according to Microsoft's documentation.
ABP Framework will not affect to your APIs, you can freely implement your APIs according to the Microsoft's documentation.
Get more from https://github.com/dotnet/aspnet-api-versioning/wiki
Further information, see https://github.com/dotnet/aspnet-api-versioning/wiki
## Sample source code
You can get the complete sample source code in https://github.com/abpframework/abp-samples/tree/master/Api-Versioning
Follow the link below to get the sample's complete source-code
https://github.com/abpframework/abp-samples/tree/master/Api-Versioning

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

@ -13,6 +13,12 @@ Dynamic C# proxies automatically handle the following stuff for you;
This system can be used by any type of .NET client to consume your HTTP APIs.
## Static vs Dynamic Client Proxies
ABP provides **two types** of client proxy generation system. This document explains the **dynamic client proxies**, which generates client-side proxies on runtime. You can also see the [Static C# API Client Proxies](Static-CSharp-API-Clients.md) documentation to learn how to generate proxies on development time.
Development-time (static) client proxy generation has a **performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**.
## Service Interface
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project, typically in the `Application.Contracts` project if you've created your solution using the startup templates.
@ -72,7 +78,7 @@ public class MyClientAppModule : AbpModule
### Endpoint Configuration
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below:
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. The simplest configuration is shown below:
```json
{
@ -204,3 +210,7 @@ public override void PreConfigureServices(ServiceConfigurationContext context)
````
This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method.
## See Also
* [Static C# Client Proxies](Static-CSharp-API-Clients.md)

217
docs/en/API/Static-CSharp-API-Clients.md

@ -0,0 +1,217 @@
# Static C# API Client Proxies
ABP can create C# API client proxy code to call your remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level details to call remote services and get results.
Static C# proxies automatically handle the following stuff for you;
* Maps C# **method calls** to remote server **HTTP calls** by considering the HTTP method, route, query string parameters, request payload and other details.
* **Authenticates** the HTTP Client by adding access token to the HTTP header.
* **Serializes** to and deserialize from JSON.
* Handles HTTP API **versioning**.
* Add **correlation id**, current **tenant** id and the current **culture** to the request.
* Properly **handles the error messages** sent by the server and throws proper exceptions.
This system can be used by any type of .NET client to consume your HTTP APIs.
## Static vs Dynamic Client Proxies
ABP provides **two types** of client proxy generation system. This document explains the **static client proxies**, which generates client-side code in your development time. You can also see the [Dynamic C# API Client Proxies](Dynamic-CSharp-API-Clients.md) documentation to learn how to use proxies generated on runtime.
Development-time (static) client proxy generation has a **performance advantage** since it doesn't need to obtain the HTTP API definition on runtime. However, you should **re-generate** the client proxy code whenever you change your API endpoint definition. On the other hand, dynamic client proxies are generated on runtime and provides an **easier development experience**.
## Service Interface
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project, typically in the `Application.Contracts` project if you've created your solution using the startup templates.
Example:
````csharp
public interface IBookAppService : IApplicationService
{
Task<List<BookDto>> GetListAsync();
}
````
> Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition.
Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint.
## Client Proxy Generation
First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project:
````
Install-Package Volo.Abp.Http.Client
````
Then add `AbpHttpClientModule` dependency to your module:
````csharp
[DependsOn(typeof(AbpHttpClientModule))] //add the dependency
public class MyClientAppModule : AbpModule
{
}
````
Now, it's ready to configure the application for the static client proxy generation. Example:
````csharp
[DependsOn(
typeof(AbpHttpClientModule), //used to create client proxies
typeof(BookStoreApplicationContractsModule) //contains the application service interfaces
)]
public class MyClientAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Prepare for static client proxy generation
context.Services.AddStaticHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly
);
}
}
````
`AddStaticHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, and prepares for static client proxy generation.
> The [application startup template](../Startup-Templates/Application.md) comes pre-configured for the **dynamic** client proxy generation, in the `HttpApi.Client` project. If you want to switch to the **static** client proxies, change `context.Services.AddHttpClientProxies` to `context.Services.AddStaticHttpClientProxies` in the module class of your `HttpApi.Client` project.
### Endpoint Configuration
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. The simplest configuration is shown below:
```json
{
"RemoteServices": {
"Default": {
"BaseUrl": "http://localhost:53929/"
}
}
}
```
See the *AbpRemoteServiceOptions* section below for more detailed configuration.
### Code Generation
Server side must be up and running while generating the client proxy code. So, run your application that serves the HTTP APIs on the `BaseUrl` that is configured like explained in the *Endpoint Configuration* section.
Open a command-line terminal in the root folder of your client project (`.csproj`) and type the following command:
````bash
abp generate-proxy -t csharp -u http://localhost:53929/
````
> If you haven't installed yet, you should install the [ABP CLI](../CLI.md).
This command should generate the following files under the `ClientProxies` folder:
![generated-static-client-proxies](../images/generated-static-client-proxies.png)
`BookClientProxy.Generated.cs` is the actual generated proxy class in this example. `BookClientProxy` is a `partial` class where you can write your custom code (ABP won't override it). `app-generate-proxy.json` contains information about the remote HTTP endpoint, so ABP can properly perform HTTP requests.
> `generate-proxy` command generates proxies for only the APIs you've defined in your application. If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../CLI.md) documentation for other options.
## Usage
It's straightforward to use the client proxies. Just inject the service interface in the client application code:
````csharp
public class MyService : ITransientDependency
{
private readonly IBookAppService _bookService;
public MyService(IBookAppService bookService)
{
_bookService = bookService;
}
public async Task DoItAsync()
{
var books = await _bookService.GetListAsync();
foreach (var book in books)
{
Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}");
}
}
}
````
This sample injects the `IBookAppService` service interface defined above. The static client proxy implementation makes an HTTP call whenever a service method is called by the client.
## Configuration
### AbpRemoteServiceOptions
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can configure it in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) to set or override it. Example:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<AbpRemoteServiceOptions>(options =>
{
options.RemoteServices.Default =
new RemoteServiceConfiguration("http://localhost:53929/");
});
//...
}
````
### Multiple Remote Service Endpoints
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file:
````json
{
"RemoteServices": {
"Default": {
"BaseUrl": "http://localhost:53929/"
},
"BookStore": {
"BaseUrl": "http://localhost:48392/"
}
}
}
````
`AddStaticHttpClientProxies` method can get an additional parameter for the remote service name. Example:
````csharp
context.Services.AddStaticHttpClientProxies(
typeof(BookStoreApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "BookStore"
);
````
`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
### Retry/Failure Logic & Polly Integration
If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class.
**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to re-try 3 times on a failure**
````csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpHttpClientBuilderOptions>(options =>
{
options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) =>
{
clientBuilder.AddTransientHttpErrorPolicy(policyBuilder =>
policyBuilder.WaitAndRetryAsync(
3,
i => TimeSpan.FromSeconds(Math.Pow(2, i))
)
);
});
});
}
````
This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method.
## See Also
* [Dynamic C# Client Proxies](Dynamic-CSharp-API-Clients.md)

24
docs/en/API/Swagger-Integration.md

@ -11,7 +11,7 @@ ABP Framework offers a prebuilt module for full Swagger integration with small c
> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually.
If installation is needed, it is suggested to use the [ABP CLI](CLI.md) to install this package.
If installation is needed, it is suggested to use the [ABP CLI](../CLI.md) to install this package.
### Using the ABP CLI
@ -48,9 +48,9 @@ First, we need to use `AddAbpSwaggerGen` extension to configure Swagger in `Conf
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var services = contex.Services;
var services = context.Services;
//... other configarations.
//... other configurations.
services.AddAbpSwaggerGen(
options =>
@ -81,11 +81,27 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
}
```
### Hide ABP Endpoints on Swagger UI
If you want to hide ABP's default endpoints, call the `HideAbpEndpoints` method in your Swagger configuration as shown in the following example:
```csharp
services.AddAbpSwaggerGen(
options =>
{
//... other options
//Hides ABP Related endpoints on Swagger UI
options.HideAbpEndpoints();
}
)
```
## Using Swagger with OAUTH
For non MVC/Tiered applications, we need to configure Swagger with OAUTH to handle authorization.
> ABP Framework uses IdentityServer by default. To get more information about IDS, check this [documentation](Modules/IdentityServer.md).
> ABP Framework uses IdentityServer by default. To get more information about IDS, check this [documentation](../Modules/IdentityServer.md).

2
docs/en/AspNet-Boilerplate-Migration-Guide.md

@ -1,4 +1,4 @@
# ASP.NET Boilerplate v5+ to ABP Framework Migration
# Migrating from ASP.NET Boilerplate to the ABP Framework
ABP Framework is **the successor** of the open source [ASP.NET Boilerplate](https://aspnetboilerplate.com/) framework. This guide aims to help you to **migrate your existing solutions** (you developed with the ASP.NET Boilerplate framework) to the ABP Framework.

38
docs/en/Autofac-Integration.md

@ -1,12 +1,12 @@
# Autofac Integration
Autofac is one of the most used dependency injection frameworks for .Net. It provides some advanced features compared to .Net Core standard DI library, like dynamic proxying and property injection.
[Autofac](https://autofac.org/) is one of the most used dependency injection frameworks for .NET. It provides advanced features compared to .Net Core's standard DI library, like dynamic proxying and property injection.
## Install Autofac Integration
> All startup templates and samples are Autofac integrated. So, most of the time you don't need to manually install this package.
> All the [startup templates](Startup-Templates/Index.md) and samples are Autofac integrated. So, most of the time you don't need to manually install this package.
Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to your project (for a multi-projects application, it's suggested to add to the executable/web project.)
Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to your project (for a multi-projects application, it's suggested to add to the executable/web project).
````
Install-Package Volo.Abp.Autofac
@ -52,6 +52,23 @@ public class Program
}
````
If you are using the static `WebApplication` class, you can call the `UseAutofac()` extension method as shown below:
````csharp
public class Program
{
public async static Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseAutofac(); // Integrate Autofac!
await builder.AddApplicationAsync<MyProjectNameWebModule>();
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
}
}
````
### Console Application
Call `UseAutofac()` method in the `AbpApplicationFactory.Create` options as shown below:
@ -79,3 +96,18 @@ namespace AbpConsoleDemo
}
````
## Using the Autofac Registration API
If you want to use Autofac's advanced [registration API](https://autofac.readthedocs.io/en/latest/register/registration.html), you need to access the `ContainerBuilder` object. [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package defines the `IServiceCollection.GetContainerBuilder()` extension method to obtain the `ContainerBuilder` object.
**Example: Get the `ContainerBuilder` object in the `ConfigureServices` method of your [module class](Module-Development-Basics.md)**
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var containerBuilder = context.Services.GetContainerBuilder();
containerBuilder.RegisterType<MyService>(); // Using Autofac's registration API
}
````
> You should install the [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to the project that you want to use the Autofac API.

17
docs/en/Background-Jobs.md

@ -155,8 +155,6 @@ public class MyModule : AbpModule
}
````
> Default background manager (see below) does not support multiple processes execute the same job queue. So, if you have multiple running instance of your application and you are using the default background job manager, you should only enable one application instance process the job queue.
## Default Background Job Manager
ABP framework includes a simple `IBackgroundJobManager` implementation that;
@ -189,12 +187,23 @@ public class MyModule : AbpModule
### Data Store
The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction. So, you can replace the implementation if you want.
The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction to store the jobs.
Background Jobs module implements `IBackgroundJobStore` using various data access providers. See its own [documentation](Modules/Background-Jobs.md).
Background Jobs module implements `IBackgroundJobStore` using various data access providers. See its own [documentation](Modules/Background-Jobs.md). If you don't want to use this module, you should implement the `IBackgroundJobStore` interface yourself.
> Background Jobs module is already installed to the startup templates by default and it works based on your ORM/data access choice.
### Clustered Deployment
The default background job manager is compatible with [clustered environments](Deployment/Clustered-Environment.md) (where multiple instances of your application run concurrently). It uses a [distributed lock](Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time.
However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, **please follow the [distributed lock](Distributed-Locking.md) document to configure a provider for your application**, if it is not already configured.
If you don't want to use a distributed lock provider, you may go with the following options:
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs).
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance.
## Integrations
Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations.

4
docs/en/Background-Workers-Hangfire.md

@ -1,6 +1,6 @@
# Hangfire Background Worker Manager
[Hangfire](https://https://www.hangfire.io/) is an advanced background jobs and worker manager. You can integrate Hangfire with the ABP Framework to use it instead of the [default background worker manager](Background-Workers.md).
[Hangfire](https://www.hangfire.io/) is an advanced background jobs and worker manager. You can integrate Hangfire with the ABP Framework to use it instead of the [default background worker manager](Background-Workers.md).
The major advantage is that you can use the same server farm to manage your Background Jobs and Workers, as well as leverage the advanced scheduling that is available from Hangfire for [Recurring Jobs](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html?highlight=recurring), aka Background Workers.
@ -126,4 +126,4 @@ context.ServiceProvider
So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.

17
docs/en/Background-Workers.md

@ -86,20 +86,20 @@ After creating a background worker class, you should add it to the `IBackgroundW
[DependsOn(typeof(AbpBackgroundWorkersModule))]
public class MyModule : AbpModule
{
public override void OnApplicationInitialization(
public override Task OnApplicationInitializationAsync(
ApplicationInitializationContext context)
{
context.AddBackgroundWorker<PassiveUserCheckerWorker>();
context.AddBackgroundWorkerAsync<PassiveUserCheckerWorker>();
}
}
````
`context.AddBackgroundWorker(...)` is a shortcut extension method for the expression below:
`context.AddBackgroundWorkerAsync(...)` is a shortcut extension method for the expression below:
````csharp
context.ServiceProvider
await context.ServiceProvider
.GetRequiredService<IBackgroundWorkerManager>()
.Add(
.AddAsync(
context
.ServiceProvider
.GetRequiredService<PassiveUserCheckerWorker>()
@ -126,10 +126,11 @@ Background workers only work if your application is running. If you host the bac
Be careful if you run multiple instances of your application simultaneously in a clustered environment. In that case, every application runs the same worker which may create conflicts if your workers are running on the same resources (processing the same data, for example).
If that's a problem for your workers, you have two options;
If that's a problem for your workers, you have the following options:
* Disable the background worker system using the `AbpBackgroundWorkerOptions` described above, for all the application instances, except one of them.
* Disable the background worker system for all the application instances and create another special application that runs on a single server and execute the workers.
* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](../Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed.
* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers.
* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance.
## Integrations

2
docs/en/Blog-Posts/2021-04-05 CmsKit/POST.md

@ -79,7 +79,7 @@ For example,
We've covered the initial features, installation and configuration steps in this post. You can read the [open-source](https://docs.abp.io/en/abp/latest/Modules/Cms-Kit) and [commercial](https://docs.abp.io/en/commercial/latest/modules/CMS-Kit) documentation to get further information about features and the CMS Kit module. CMS Kit's initial version contains lots of features that you can easily integrate and use in your applications. We're planning to improve the existing features, fixing bugs and adding new features in upcoming releases. If you want to give some feedback or have a feature request, please reach out to us from [GitHub](https://github.com/abpframework/abp) or [support.abp.io](https://support.abp.io). We will be happy to plan the CMS Kit module's future together.
We've covered the initial features, installation and configuration steps in this post. You can read the [open-source](https://docs.abp.io/en/abp/latest/Modules/Cms-Kit/Index) and [commercial](https://docs.abp.io/en/commercial/latest/modules/CMS-Kit) documentation to get further information about features and the CMS Kit module. CMS Kit's initial version contains lots of features that you can easily integrate and use in your applications. We're planning to improve the existing features, fixing bugs and adding new features in upcoming releases. If you want to give some feedback or have a feature request, please reach out to us from [GitHub](https://github.com/abpframework/abp) or [support.abp.io](https://support.abp.io). We will be happy to plan the CMS Kit module's future together.
Thank you!

196
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/POST.md

@ -0,0 +1,196 @@
# ABP.IO Platform 5.2 RC.1 Has Been Released
Today, we are happy to release the [ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) version **5.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version.
> **The planned release date for the [5.2.0 Stable](https://github.com/abpframework/abp/milestone/66) version is April 05, 2022**.
Please try this version and provide feedback for a more stable ABP version 5.2! Thank you all.
## Get Started with the 5.2 RC
Follow the steps below to try the version 5.2.0 RC today:
1) **Upgrade** the ABP CLI to version `5.2.0-rc.1` using a command line terminal:
````bash
dotnet tool update Volo.Abp.Cli -g --version 5.2.0-rc.1
````
​ **or install** it if you haven't before:
````bash
dotnet tool install Volo.Abp.Cli -g --version 5.2.0-rc.1
````
2) Create a **new application** with the `--preview` option:
````bash
abp new BookStore --preview
````
See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options.
> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**.
You can use any IDE that supports .NET 6.x, like **[Visual Studio 2022](https://visualstudio.microsoft.com/downloads/)**.
## Migration Guides
Please see the migration guides if you are upgrading from 5.x versions:
* [ABP Framework 5.x to 5.2 migration guide](https://docs.abp.io/en/abp/5.2/Migration-Guides/Abp-5_2)
* [ABP Commercial 5.x to 5.2 migration guide](https://docs.abp.io/en/commercial/5.2/migration-guides/index)
## What's New with ABP Framework 5.2?
In this section, I will introduce some major features released with this version. Here, a brief list of titles explained in the next sections:
* Single-layer Solution Template
* API Versioning
* libs Folder Has been Removed from Source Control
* Hiding the Default ABP Endpoints from Swagger UI
* Custom Global CSS and JavaScript for the CMS Kit module
* Other news
Let's begin with the first section.
### Single-layer Solution Template
ABP's [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application) is a well-organized and layered solution to create maintainable business applications. However, some developers find it a little bit complex for simple and short-term applications. For such applications, we've created a new startup template that has no layering and built as simple as possible. It has the same functionality, features and modules on runtime, but the development model is minimal and everything is in the single project (`csproj`), as shown in the following figure:
![single-layer-ABP-Dotnet-solution](single-layer-solution.png)
Use the `app-nolayers` as the template name while creating your solution:
````bash
abp new BookStore -t app-nolayers --preview
````
[ABP Commercial](https://commercial.abp.io/) developers can use the `app-nolayers-pro` as the startup template:
````csharp
abp new BookStore -t app-nolayers-pro --preview
````
> There is a bug for the `app-nolayers-pro` related to the licensing system, which will be fixed with 5.2.0-rc.2: `appsettings.secrets.json` is missing in the project folder (should be near to `appsettings.json` and contains the license code normally). As a workaround, create a new solution with the layered startup template, find and copy that file to the single-layer solution.
One note about the single-layer project is it doesn't have Blazor WebAssembly UI, because it requires 3 projects at least (one server-side, one UI and one shared library project between these two). We will consider to add Blazor UI support based on your feedback. You can continue to develop Blazor WASM projects using the standard layered solution template.
#### Database Migrations for EF Core
After creating your solution, you need to create the database before running the application. We've added a parameter to the application that can be specified to migrate the database and seed the initial data. Open the project's directory (that contains the `csproj` file) in a command-line terminal and type the following command:
````bash
dotnet run --migrate-database
````
It will run, migrate the database and exit. You can then run the application as normal.
You could use the standard `dotnet ef database update` command (or `Update-Database` command in Visual Studio's Package Manager Console). It can successfully create the database tables. However, it doesn't seed the initial data that is necessary to run the application.
To keep the solution simple, we haven't added an external application (like the `DbMigrator` in the layered application startup template) to migrate the database.
Using the same application to migrate the database is simple and useful for development environment, and it can also be used in production environment. However, there are other ways of migrating a database. Please read more on [Microsoft's documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations).
#### Other UI and Database Options
The new single-layer solution template also supports Angular and Blazor UI, and MongoDB for the database side. For the UI, you can use `mvc` (default), `angular` or `blazor-server` with the `-u` (or `--ui`) parameter. For the database provider, you can use `ef` (default) or `mongodb` with the `-d` (or `--database-provider`) parameter. Example:
````bash
abp new BookStore -t app-nolayers -u angular -d mongodb --preview
````
This will create a single layer template with Angular as the UI framework and MongoDB as the database provider.
#### Single-Layer Tutorial
[Hamza Albreem](https://twitter.com/st_braim) has created a video tutorial to explain how to develop a simple application with this startup template.
![video-single-layer-tutorial](video-single-layer-tutorial.png)
You can [watch it here](https://community.abp.io/posts/developing-a-minimalist-application-with-the-abp-framework-mvad01ca).
### API Versioning
API versioning has always been possible with the ABP Framework, but we haven't had a documentation for it. With version 5.2, we've created a document to explain how to implement API versioning for your applications and add versioning support to your standard application service classes. See the [documentation here](https://docs.abp.io/en/abp/5.2/API/API-Versioning).
### libs Folder Has been Removed from Source Control
> **NOTICE: This can be a breaking change for your development environment and CI/CD pipelines. So, please read it carefully and take the necessary actions.**
When you create solutions with MVC (Razor Pages) or Blazor Server UI, your application will have a `wwwroot/libs` folder in the UI project as shown below:
![libs-folder](libs-folder.png)
The `libs` folder contains all the client-side (mostly JavaScript and CSS) library dependencies. For example, in the figure above, the `bootstap` folder contains the necessary files for the Bootstrap library. The folder's content is copied from the `node_modules` folder (it only copies the minimum files to make the library work, not the whole folder) with ABP CLI's `abp install-libs` command.
Before version 5.2, the `libs` folder was coming with the startup template and committed into your source control system (e.g. Git). With version 5.2, this folder is excluded from the source control by default, so every developer getting the solution must run `abp install-libs` in the UI project's root directory to install these libraries. This approach saves a huge amount of size of the solution. For example, the initial size of an MVC UI application reduces from `9.83MB` to `0.23MB` (you read it right!).
When you create a new solution with ABP CLI, `abp install-libs` command is automatically executed, so your application directly works. However, if your teammates (or CI/CD system) get the solution from the source control, they should run the `abp install-libs` before running the solution. If you don't want that, you can simply remove the `**/wwwroot/libs/*` line from the `.gitignore` file in the root folder of your solution, then the `libs` folder is added to your source control again (if you are using a source control system other than Git, you should apply your system's rules to include/exclude that folder).
### Hiding the Default ABP Endpoints from Swagger UI
[Engincan Veske](https://twitter.com/EngincanVeske) had written [an article](https://community.abp.io/posts/how-to-hide-abp-related-endpoints-on-swagger-ui-mb2w01fe) to explain how to hide ABP's default endpoints from Swagger UI. Then We thought that could be a good built-in option in the ABP Framework and added a `HideAbpEndpoints` method to the `AddAbpSwaggerGen` method, which can be used as in the following code example:
````csharp
services.AddAbpSwaggerGen(
options =>
{
//... other options
//Hides ABP Related endpoints on Swagger UI
options.HideAbpEndpoints();
}
)
````
After that, ABP's default endpoints will still be functional, but will be hidden in Swagger UI.
### Custom Global CSS and JavaScript for the CMS Kit module
We are improving the [CMS Kit module](https://docs.abp.io/en/abp/5.2/Modules/Cms-Kit/Index) and adding new features constantly. A new feature with version 5.2 is a global resources system, where you can write custom JavaScript or CSS code on the application's UI (added a new page for it), which will be immediately available in all your application pages:
![cms-kit-global-resources](cms-kit-global-resources.png)
In this way, you can customize your application's look and behavior on runtime. See [the documentation](https://docs.abp.io/en/abp/5.2/Modules/Cms-Kit/Global-Resources) for more information.
> Note that the [pages](https://docs.abp.io/en/abp/5.2/Modules/Cms-Kit/Pages) feature already have a page-basis script/style editor. But this new feature allows you to write script/style that is applied to all pages of your application.
### Other news
* Upgraded the [Blazorise](https://blazorise.com/) library to v1.0 for Blazor UI. After the upgrade, ensure that all Blazorise-related packages are using v1.0 in your application.
If you want to see more details, you can check [the release on GitHub](https://github.com/abpframework/abp/releases/tag/5.2.0-rc.1), that contains a list of all issues and pull requests closed with this version.
## What's New with ABP Commercial 5.2?
We've also made many enhancements for [ABP Commercial](https://commercial.abp.io/), and also made the necessary changes and improvements to align with the v5.2 release of the ABP Framework. For example, the **single-layer solution template** is also available for ABP Commercial as explained above.
### Many to Many Relations on Code Generation
One exciting new feature with ABP Commercial v5.2 is supporting many-to-many entity relations while generating CRUD code with [ABP Suite](https://commercial.abp.io/tools/suite).
When you open ABP Suite v5.2, you will see an "Add navigation collection" button in the Navigation tab. Here, you can click that button, select the target entity and other options:
![suite-many-to-many-entity-relation](suite-many-to-many-entity-relation.png)
With this new feature, you can automatically generate advanced user interfaces without touching the code, then customize the generated code to implement your business logic.
## Community News
We organized the 3rd live [ABP Community Talks](https://community.abp.io/events) event on February 23rd. ABP community has a good interest in these events and we will continue to organize such a live event in every month. March's event will be announced in a few days. [Follow us on twitter](https://twitter.com/abpframework).
[ABP Community](https://community.abp.io/) website is being a huge resource of articles and video tutorials on the ABP Framework and .NET. There have been 93 articles/tutorials submitted so far. Here's a list of a few contents posted in the last weeks:
* [Ahmet Urel](https://twitter.com/YellowDraqon) submitted a series of articles to demonstrate the usage of [MudBlazor](https://www.mudblazor.com/) library with the ABP Framework Blazor UI: [Part 1](https://community.abp.io/posts/mudblazor-theme-in-abp-blazor-webassembly-ae23zz17), [Part 2](https://community.abp.io/posts/mudblazor-theme-in-abp-blazor-webassembly-part-2-tkvrvyvm) and [Part 3](https://community.abp.io/posts/mudblazor-theme-in-abp-blazor-webassembly-part-3-c8hwx00l).
* [Enis Necipoğlu](https://twitter.com/EnisNecipoglu) has created two articles for ABP & **.NET MAUI** integration: See [Part 1](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf) and [Part 2](https://community.abp.io/posts/using-abp-client-proxies-in-maui-with-openid-connect-em7x1s8k). [Bart Van Hoey](https://twitter.com/bartvanhoey) also created [an article](https://community.abp.io/posts/abp-framework-consumed-by-a-.net-maui-app-e74fmblw) for .NET MAUI and the ABP Framework.
* [Enis Necipoğlu](https://twitter.com/EnisNecipoglu) has also created [an article](https://community.abp.io/posts/using-autofilterer-with-abp-framework-uuqv81jm) to demonstrate how to use his own open source [AutoFilterer](https://github.com/enisn/AutoFilterer) library with the ABP Framework.
* [Jonathan Potts](https://github.com/jonathanpotts) has created his first ABP Community article that shows how to use Bootswatch themes with the ABP Framework. [See it here](https://community.abp.io/posts/customizing-the-abp-basic-theme-with-bootswatch-4luoqzr0).
Thanks to all the contributors. It is appreciated if you want to submit your post and share your knowledge with the ABP community: https://community.abp.io/posts/submit
## Conclusion
This version was mostly about minor features and enhancements for existing features, and getting the framework, architecture and solution structure more mature. Currently, we are working on middle and long term features, changes and goals. You can find most of them on the [road map here](https://docs.abp.io/en/abp/5.2/Road-Map).
The planned release date for the [5.2.0 Stable](https://github.com/abpframework/abp/milestone/66) version is April 05, 2022. Please try the ABP v5.2 RC and provide feedback to have a more stable release.

BIN
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/cms-kit-global-resources.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/libs-folder.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/single-layer-solution.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/suite-many-to-many-entity-relation.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
docs/en/Blog-Posts/2022-03-08 v5_2_Preview/video-single-layer-tutorial.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

51
docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md

@ -0,0 +1,51 @@
# ABP.IO Platform 5.2 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 5.2 versions have been released today.
## What's New With 5.2?
Since all the new features are already explained in details with the [5.2 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published) for all the features and enhancements.
## Creating New Solutions
You can create a new solution with the ABP Framework version 5.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started).
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more.
## How to Upgrade an Existing Solution
### Install/Update the ABP CLI
First of all, install the ABP CLI or upgrade to the latest version.
If you haven't installed yet:
```bash
dotnet tool install -g Volo.Abp.Cli
```
To update an existing installation:
```bash
dotnet tool update -g Volo.Abp.Cli
```
### ABP UPDATE Command
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command:
```bash
abp update
```
Run this command in the root folder of your solution.
## Migration Guide
Check [the migration guide](https://docs.abp.io/en/abp/5.2/Migration-Guides/Abp-5_2) for the applications with the version 5.x upgrading to the version 5.2.
## About the Next Version
The next feature version will be 5.3. It is planned to release the 5.3 RC (Release Candidate) on May 03 and the final version on May 31, 2022. You can follow the [release planning here](https://github.com/abpframework/abp/milestones).
Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problem with this version.

22
docs/en/CLI.md

@ -18,13 +18,13 @@ dotnet tool update -g Volo.Abp.Cli
## Global Options
While each command may have a set of options, there are some global options those can be used with any command;
While each command may have a set of options, there are some global options that can be used with any command;
* `--skip-cli-version-check`: Skips to check the latest version of the ABP CLI. If you don't specify, it will check the latest version and shows a warning message if there is a newer version of the ABP CLI.
## Commands
Here, the list of all available commands before explaining their details:
Here, is the list of all available commands before explaining their details:
* **`help`**: Shows help on the usage of the ABP CLI.
* **`new`**: Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md).
@ -106,6 +106,16 @@ For more samples, go to [ABP CLI Create Solution Samples](CLI-New-Command-Sample
* **`module`**: [Module template](Startup-Templates/Module.md). Additional options:
* `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI).
* **`console`**: [Console template](Startup-Templates/Console.md).
* **`app-nolayers`**: Application single layer template.
* `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks:
* `mvc`: ASP.NET Core MVC.
* `angular`: Angular UI.
* `blazor-server`: Blazor Server UI.
* `none`: Without UI.
* `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers:
* `ef`: Entity Framework Core.
* `mongodb`: MongoDB.
* `--pwa`: Specifies to Configure Angular or Blazor WebAssembly project as Progressive Web Application.
* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory.
* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version.
* `--preview`: Use latest preview version.
@ -289,7 +299,7 @@ abp generate-proxy -t <client-type> [options]
Examples:
````bash
abp generate-proxy -t ng
abp generate-proxy -t ng -url https://localhost:44302/
abp generate-proxy -t js -url https://localhost:44302/
abp generate-proxy -t csharp -url https://localhost:44302/
````
@ -303,12 +313,13 @@ abp generate-proxy -t csharp -url https://localhost:44302/
* `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`.
* `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
* `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
* `--url`: Specifies api definition url. Default value is API Name's url in environment file.
* `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
* `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client:
* `--output` or `-o`: JavaScript file path or folder to place generated code in.
* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`.
* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types.
* `--url` or `-u`: API definition URL from. For `csharp` and `js` client types.
* `--url` or `-u`: API definition URL from.
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.
@ -341,12 +352,13 @@ abp remove-proxy -t csharp --folder MyProxies/InnerFolder
* `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`.
* `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
* `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
* `--url`: Specifies api definition url. Default value is API Name's url in environment file.
* `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
* `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client:
* `--output` or `-o`: JavaScript file path or folder to place generated code in.
* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`.
* `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types.
* `--url` or `-u`: API definition URL from. For `csharp` and `js` client types.
* `--url` or `-u`: API definition URL from.
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.

8
docs/en/Caching.md

@ -1,7 +1,9 @@
# Caching
# Distributed Caching
ABP Framework extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed).
> **Default implementation of the `IDistributedCache` interface is` MemoryDistributedCache` which works in-memory.** See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use Redis as the distributed cache server.
## Installation
> This package is already installed by default with the [application startup template](Startup-Templates/Application.md). So, most of the time, you don't need to install it manually.
@ -27,8 +29,6 @@ ASP.NET Core defines the `IDistributedCache` interface to get/set the cache valu
> `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications, but also available to **any type of applications**.
> Default implementation of the `IDistributedCache` interface is the `MemoryDistributedCache` which works **in-memory**. See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use the Redis as the distributed cache server.
See [ASP.NET Core's distributed caching document](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more information.
### `IDistributedCache<TCacheItem>` Interface
@ -258,7 +258,7 @@ ABP's distributed cache interfaces provide methods to perform batch methods thos
Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect.
`IDistributedCache<..>` methods gets an optional parameter, named `considerOuw`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
### IDistributedCacheSerializer

2
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/POST.md

@ -178,7 +178,7 @@ module.exports = {
};
```
> ABP copies related packages from **node_modules** folder to the **libs** folder by examining this file. You can read this [document](docs.abp.io/en/abp/latest/UI/AspNetCore/Client-Side-Package-Management#mapping-the-library-resources) for more info.
> ABP copies related packages from **node_modules** folder to the **libs** folder by examining this file. You can read this [document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Client-Side-Package-Management#mapping-the-library-resources) for more info.
* Then run the `abp install-libs` to install the dependencies and copy them into the libs folder by your mappings configuration. After running this command, in your **libs** folder it should be a folder named **syncfusion** folder.

2
docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md

@ -108,7 +108,7 @@ public class SwaggerSettingConsts
We've created a class with a constant variable to avoid using the magic strings. This variable will be our setting name.
ABP provides us a [Settings System](https://docs.abp.io/en/abp/latest/Setting) to easily define settings for our applications. We only need to create a class that derives from the `SettingDefinitionProvider` class, but we don't even need to do this because the ABP startup templates come with a pre-defined setting provider class.
ABP provides us a [Settings System](https://docs.abp.io/en/abp/latest/Settings) to easily define settings for our applications. We only need to create a class that derives from the `SettingDefinitionProvider` class, but we don't even need to do this because the ABP startup templates come with a pre-defined setting provider class.
* So open the setting definition provider class (`SwaggerSettingsDemoSettingDefinitionProvider` in our case, it's under the /Settings folder of your domain layer) and update the class:

601
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md

@ -0,0 +1,601 @@
# Integrating MAUI Client via using OpenID Connect
This is a demonstration for connecting ABP backend from MAUI app via using openid connect.
In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application.
> This is by intent. The code flow does not allow the user to log in using a native view in the app. The reason being that this flow ensures that the username and password are never seen by the client (except the browser, which is part of the OS system - aka we trust it). You could enable using a native login view with the Resource Owner Password Credentials (ROPC) flow. But this is also an attack vector. Suppose someone makes a fraud duplicate of your application and tricking users into entering their credentials. The fraudulent app could store those credentials in-between. You just got to enjoy those tin-foil-hat moments when doing security. In other words, using the code flow does not give an attacker that opportunity and therefore is the recommended option for mobile clients.
>
> - [@Mark Allibone](https://mallibone.com/post/xamarin-oidc)
By the way, my motivation for building this sample is presenting just another way for authentication. **Resource Owner Password Credentials** authentication is already provided and it's more common way to do. This is yet another way to authenticate users.
## Source Code
You can also find source code on GitHub in ABP-Samples.
- [abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Creating projects
- Create an ABP project without UI
```bash
abp new Acme.BookStore -t app --no-ui -d mongodb --no-random-ports
```
- Create a maui application
```bash
mkdir maui
cd maui
dotnet new maui -n Acme.BookStore.MauiClient
```
There is a long way for configuring scopes and callback urls for both server and clients. We'll use [WebAuthenticator](https://docs.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android) to perform this operation.
## Configuring IdentityServer
- Go to DbMigrator folder and MAUI client in **appsettings.json**. Add following client code in **IdentityServer:Clients** path:
```json
"BookStore_Maui": {
"ClientId": "BookStore_Maui",
"ClientSecret": "1q2w3e*",
"RootUrl": "bookstore://"
}
```
- Go to **IdentityServerDataSeedContributor** in Domain project under IdentityServer folder. Append following code section into **CreateClientsAsync()** method.
```csharp
// Maui Client
var mauiClientId = configurationSection["BookStore_Maui:ClientId"];
if (!mauiClientId.IsNullOrWhiteSpace())
{
var mauiRootUrl = configurationSection["BookStore_Maui:RootUrl"];
await CreateClientAsync(
name: mauiClientId,
scopes: commonScopes,
grantTypes: new[] { "authorization_code" },
secret: configurationSection["BookStore_Maui:ClientSecret"]?.Sha256(),
requireClientSecret: false,
redirectUri: $"{mauiRootUrl}"
);
}
```
- Run DbMigrator
- Then run HttpApi.Host
### Configuring NGROK
Client will check configuration from `/.well-known/openid-configuration` path and it must be a secured connection between client & server. I prefer to use ngrok to open my backend app to entire web.
- Go to [getting started](https://dashboard.ngrok.com/get-started/setup) page of ngrok _(login or register first)_ and download the ngrok tool.
- Don't forget to login from tool:
```bash
ngrok authtoken XXX
```
_A sample command is being displayed at dashboard where you download ngrok from_
- Open your HttpApi.Host with ngrok
```bash
.\ngrok.exe http https://localhost:44350
```
- You'll see a generated xxx.ngrok.io url. Navigate to `/.well-known/openid-configuration` to check if it's working right.
You should see something like that:
![](art/openid-configuration.png)
Issuer must be your URL, not localhost! If you see still localhost, try to disable host header rewrite.
- Also, ValidIssuers must be defined to validate tokens.
- Add **ValidIssuers** section to your `appsettings.json` of HttpApi.Host
```js
"AuthServer": {
"Authority": "https://localhost:44350",
"RequireHttpsMetadata": "false",
"SwaggerClientId": "BookStore_Swagger",
"SwaggerClientSecret": "1q2w3e*",
"ValidIssuers": [
"https://46fd-45-156-29-175.ngrok.io"
]
},
```
- Then define it in **ConfigureAuthentication** method in Module class
```csharp
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
// ...
options.TokenValidationParameters.ValidIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get<string[]>();
});
}
```
We're done with backend. Let's continue with MAUI app.
## Developing MAUI App
Before we go, there is something to do like configuring dependency injection to get rid of unnecessary huge class coupling.
### Configuring Dependency Injection
- Go to **MauiApplication** class and add `MainPage` in services.
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
```
- And inject MainPage from constructor in **App.xaml.cs**
```csharp
public App(MainPage mainPage)
{
InitializeComponent();
MainPage = mainPage;
}
```
Now MainPage is ready for injecting dependencies to it.
### Configuring OIDC
- Add `IdentityModel.OidcClient` package to project
```xml
<ItemGroup>
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
</ItemGroup>
```
- Create **WebAuthenticatorBrowser**
```csharp
internal class WebAuthenticatorBrowser : IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
WebAuthenticatorResult authResult =
await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(options.EndUrl));
var authorizeResponse = ToRawIdentityUrl(options.EndUrl, authResult);
return new BrowserResult
{
Response = authorizeResponse
};
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return new BrowserResult()
{
ResultType = BrowserResultType.UnknownError,
Error = ex.ToString()
};
}
}
public string ToRawIdentityUrl(string redirectUrl, WebAuthenticatorResult result)
{
IEnumerable<string> parameters = result.Properties.Select(pair => $"{pair.Key}={pair.Value}");
var values = string.Join("&", parameters);
return $"{redirectUrl}#{values}";
}
}
```
- Configure **OidcClient** in **MauiProgram**
```csharp
builder.Services.AddTransient<WebAuthenticatorBrowser>();
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore",
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Go to **MainPage.xaml**, remove everyting and add a button for login
```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage">
<ScrollView>
<Grid RowSpacing="25" RowDefinitions="Auto,Auto,Auto,Auto,*"
Padding="{OnPlatform iOS='30,60,30,30', Default='30'}">
<Button Text="Click to Log In" Clicked="OnLoginClicked" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
</Grid>
</ScrollView>
</ContentPage>
```
- Inject **OidcClient** in **MainPage.xaml.cs** and make login operation.
```csharp
using IdentityModel.OidcClient;
namespace Acme.BookStore.MauiClient;
public partial class MainPage : ContentPage
{
protected OidcClient OidcClient { get; }
public MainPage(OidcClient oidcClient)
{
InitializeComponent();
OidcClient = oidcClient;
}
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
}
```
It still won't work because there is something more to do for each platform. Check out the next step and configure the platforms that you're using.
## Platform Specific Configurations
Each platform (UWP, OSX, iOS and Android) requires some configuration to use authentication from browser. In that step, we'll open a browser and user will login on the browser. After that, as you see in IdentityServer configurations, IdentityServer will redirect 'bookstore://' url that only contains scheme and that scheme is not http. Our application will handle that scheme and will be launched with parameters.
### Android
- Start with creating a new Activity named **BookStoreWebAuthenticatorCallbackActivity**
```csharp
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Acme.BookStore.MauiClient.Platforms.Android;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = CALLBACK_SCHEME)]
public class BookStoreWebAuthenticatorCallbackActivity : Microsoft.Maui.Essentials.WebAuthenticatorCallbackActivity
{
const string CALLBACK_SCHEME = "bookstore";
}
```
- Add `CustomTabsService` to **AndroidManifest.xml** as below. _(queries tags only.)_
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
```
> For some reason, an error occurs with my emulator while targeting SDK 31, so I've changed the target SDK to 30.
- Run the Application and perform a login operation.
AccessToken will be retrieved.
![abp-maui-android-openid-connect](art/android-login-demo.gif)
---
### iOS/MacCatalyst
- Add following key to **Info.plist**
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>mauiessentials</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bookstore</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
```
- Open **AppDelegate** class and override `OpenUrl` and `ContinueUserActivity` methods
```csharp
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (Microsoft.Maui.Essentials.Platform.OpenUrl(app, url, options))
return true;
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
if (Microsoft.Maui.Platform.ContinueUserActivity(application, userActivity, completionHandler))
return true;
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
```
- Make all steps for MacCatalyst, too.
> **Tip:** If your IDE struggles while displaying references and namespace suggestions, make sure you're displaying that file with iOS Target Framework.
>
> You'll find it at the top of the editor.
> ![abp-maui-example-compiler-select](art/compiler-select.png)
![abp-maui-ios-openid-connect](art/ios-login-demo.gif)
![abp-maui-MacCatalyst-openid-connect](art/macos-login-demo.gif)
---
### UWP (Windows)
- Add following protocol extension in `Package.appxmanifest` file.
```xml
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="bookstore">
<uap:DisplayName>BookStore</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
</Applications>
```
> Currently UWP has a bug in MAUI Essentials, I believe the MAUI team will solve it as soon as possible. You can track the status of issue:
> https://github.com/dotnet/maui/issues/2702
- That's it on Windows side. Run the application.
## Refreshing the access token
IdentityServer doesn't return a refresh token by default. So we have to add `offline_access` to our scope while sending login request.
- Add `offline_access` to scope in **MauiApplication.cs** that we configured before.
```csharp
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore offline_access", // <-- Final state must be like this.
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Then check if it's working or not in **MainPage.xaml.cs**. Update OnLoginClicked method as below
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
var refreshResult = await OidcClient.RefreshTokenAsync(loginResult.RefreshToken);
await DisplayAlert("Refresh Result", "New Access Token is: \n\n" + refreshResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
## Storing the access token
In this step we have to store access token & refresh token for future requests.
> [Secure Storage](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android) is highly recommended to store this kind of sensitive data. But it's not topic of this article. You can configure and use SecureStorage on your own. I'll go with `App Properties`.
- Add following class to store key names instead of using magic strings in code.
```csharp
namespace Acme.BookStore.MauiClient;
public static class OidcConsts
{
internal const string AccessTokenKeyName = "__access_token";
internal const string RefreshTokenKeyName = "__refresh_token";
}
```
- Then go back to **MainPage.xaml.cs** and save our tokens after a successfull login.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
// Navigate to an inner page here.
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Add following **AccessTokenHttpMessageHandler** to append AccessToken to our requests & refresh token when required.
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler
{
protected OidcClient OidcClient { get; }
public AccessTokenHttpMessageHandler(OidcClient oidcClient) : base(new HttpClientHandler())
{
OidcClient = oidcClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (App.Current.Properties.TryGetValue(OidcConsts.AccessTokenKeyName, out object currentTokenValue) && currentTokenValue != null)
{
request.SetBearerToken(currentTokenValue?.ToString());
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (App.Current.Properties.TryGetValue(OidcConsts.RefreshTokenKeyName, out object refreshTokenValue) && refreshTokenValue != null)
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshTokenValue?.ToString());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = refreshResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = refreshResult.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(refreshResult.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
else
{
var result = await OidcClient.LoginAsync(new LoginRequest());
request.SetBearerToken(result.AccessToken);
App.Current.Properties[OidcConsts.AccessTokenKeyName] = result.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = result.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(result.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
```
- Then register to dependency injection.
```csharp
builder.Services.AddSingleton<AccessTokenHttpMessageHandler>();
builder.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
BaseAddress = new Uri("https://46fd-45-156-29-175.ngrok.io")
});
```
- Now make we can send request to backend with authentication. Go to **MainPage.xaml.cs** and send a request right after authentication.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
var json = await httpClient.GetStringAsync("/api/identity/users");
await DisplayAlert("/api/identity/users", json, "close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Following result will be returned from API.
<img src="art/identity-users-request-result.png" height="480">
---
## Recap
The purpose of this arcitle is connecting to ABP backend with access token and it's working properly.
I'm planning to integrate HttpApi.Client library of backend project instead of making requests manually as a second part of this article. I'll get inspired by [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) repo to achive that.

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

529
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

529
docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

548
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md

@ -0,0 +1,548 @@
# Handle Concurrency with EF Core in an ABP Framework Project with ASP.NET Core MVC
In this article, we'll create a basic application to demonstrate how "Concurrency Check/Control" can be implemented in an ABP project.
## Creating the Solution
For this article, we will create a simple BookStore application and add CRUD functionality to the pages. Hence we deal with the concurrency situation.
We can create a new startup template with EF Core as a database provider and MVC for the UI Framework.
> If you already have a project, you don't need to create a new startup template, you can directly implement the following steps to your project. So you can skip this section.
We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI).
```bash
abp new Acme.BookStore
```
After running the above command, our project boilerplate will be downloaded. Then we can open the solution and start the development.
## Starting the Development
Let's start with defining our entities.
### Creating Entities
Create a `Book.cs` (/Books/Book.cs) class in the `.Domain` layer:
```csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
```
* To enable **Concurrency Check** for our entities, our entities should be implemented the `IHasConcurrencyStamp` interface, directly or indirectly.
* [Aggregate Root](https://docs.abp.io/en/abp/5.2/Entities#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface, so if we inherit our entities from one of these entity classes then we won't need to manually implement the `IHasConcurrencyStamp` interface.
* And we've derived the `Book` entity from `AuditedAggregateRoot<TKey>` here, so we don't need to implement the `IHasConcurrencyStamp` interface because `AuditedAggregateRoot` class already implemented the `IHasConcurrencyStamp` interface.
> You can read more details from the [Concurrency Check](https://docs.abp.io/en/abp/5.2/Concurrency-Check) documentation.
Then, create a `BookType` (/Books/BookType.cs) enum in the `.Domain.Shared` layer:
```csharp
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
```
### Database Integration
Open the `BookStoreDbContext` (/EntityFrameworkCore/BookStoreDbContext.cs) class in the `*.EntityFrameworkCore` project and add the following `DbSet<Book>` statement:
```csharp
namespace Acme.BookStore.EntityFrameworkCore;
[ReplaceDbContext(typeof(IIdentityDbContext))]
[ReplaceDbContext(typeof(ITenantManagementDbContext))]
[ConnectionStringName("Default")]
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
//Entities from the modules
public DbSet<Book> Books { get; set; } //add this line
}
```
Then we can navigate to the `OnModelCreating` method in the same class and configure our tables/entities:
```csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
//* Configure your own tables/entities inside here */
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
```
After the mapping configurations, we can create a new migration and apply changes to the database.
To do this, open your command line terminal in the directory of the `EntityFrameworkCore` project and run the below command:
```bash
dotnet ef migrations add Added_Books
```
After this command, a new migration will be generated and then we can run the `*.DbMigrator` project to apply the last changes to the database such as creating a new table named `Books` according to the last created migration.
### Defining DTOs and Application Service Interfaces
We can start to define the use cases of the application.
Create the DTO classes (under the **Books** folder) in the `Application.Contracts` project:
**BookDto.cs**
```csharp
public class BookDto : AuditedEntityDto<Guid>, IHasConcurrencyStamp
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
public string ConcurrencyStamp { get; set; }
}
```
* The `AuditedEntityDto<TKey>` class is not implemented from the `IHasConcurrencyStamp` interface, so for the **BookDto** class we need to implement the `IHasConcurrencyStamp`.
* This is important, because we need to return books with their **ConcurrencyStamp** value.
**CreateBookDto.cs**
```csharp
public class CreateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
```
**UpdateBookDto.cs**
```csharp
public class UpdateBookDto : IHasConcurrencyStamp
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
public string ConcurrencyStamp { get; set; }
}
```
* Here, we've implemented the `IHasConcurrencyStamp` interface for the **UpdateBookDto** class.
* We will use this value while updating an existing book. ABP Framework will compare the current book's **ConcurrencyStamp** value with the provided one, if values are matched, this means everything is as it is supposed to be and will update the record.
* If values are mismatched, then it means the record that we're trying to update is already updated by another user and we need to get the latest changes to be able to make changes on it.
* Also, in that case, `AbpDbConcurrencyException` will be thrown by the ABP Framework and we can either handle this exception manually or let the ABP Framework handle it on behalf of us and show a user-friendly error message as in the image below.
![](./optimistic-concurrency.png)
Create a new `IBookAppService` (/Books/IBookAppService.cs) interface in the `Application.Contracts` project:
```csharp
public interface IBookAppService :
ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto>
{
}
```
* We've implemented the `ICrudAppService` here, because we just need to perform CRUD operations and this interface helps us define common CRUD operation methods.
### Application Service Implementations
Create a `BookAppService` (/Books/BookAppService.cs) class inside the `*.Application` project and implement the application service methods, as shown below:
```csharp
public class BookAppService :
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
public override async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await Repository.GetAsync(id);
book.Name = input.Name;
book.Price = input.Price;
book.Type = input.Type;
book.PublishDate = input.PublishDate;
//set Concurrency Stamp value to the entity
book.ConcurrencyStamp = input.ConcurrencyStamp;
var updatedBook = await Repository.UpdateAsync(book);
return ObjectMapper.Map<Book, BookDto>(updatedBook);
}
}
```
* We've used the `CrudAppService` base class. This class implements all common CRUD operations and if we want to change a method, we can simply override the method and change it to our needs.
> Normally, you don't need to override the `UpdateAsync` method to do **Concurrency Check**. Because the `UpdateAsync` method of the `CrudAppService` class by default map input values to the entity. But I wanted to override this method to show what we need to do for **Concurrency Check**.
* We can look closer to the `UpdateAsync` method here, because as we've mentioned earlier we need to pass the provided **ConcurrencyStamp** value to be able to do **Concurrency Check/Control** to our entity while updating.
* At that point, if the given record is already updated by any other user, a **ConcurrencyStamp** mismatch will occur and `AbpDbConcurrencyException` will be thrown thanks to the **Concurrency Check** system of ABP, data-consistency will be provided and the current record won't be overridden.
* And if the values are matched, the record will be updated successfully.
After implementing the application service methods, we can do the related mapping configurations, so open the `BookStoreApplicationAutoMapperProfile.cs` and update the content as below:
```csharp
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateBookDto, Book>();
}
}
```
### User Interface
So far, we've applied the all necessary steps for the **Concurrency Check** system, let's see it in action.
Create a razor page in the `.Web` layer named `Index` (**/Pages/Books/Index.cshtml**), open this file and replace the content with the following code block:
```html
@page
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@model Acme.BookStore.Web.Pages.Books.Index
@section scripts
{
<abp-script src="/Pages/Books/Index.js" />
}
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<abp-card-title>Books</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="New Book"
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>
```
* We've defined a table and "New Book" button inside a card element here, we'll fill the table with our book records in the next step by using the **Datatables** library.
Create an `Index.js` (**/Pages/Books/Index.js**) file and add the following code block:
```js
$(function () {
var l = abp.localization.getResource('BookStore');
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
var dataTable = $('#BooksTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: true,
paging: true,
order: [[1, "asc"]],
searching: false,
scrollX: true,
ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
columnDefs: [
{
title: l('Actions'),
rowAction: {
items:
[
{
text: l('Edit'),
action: function (data) {
editModal.open({ id: data.record.id });
}
}
]
}
},
{
title: l('Name'),
data: "name"
},
{
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
}
},
{
title: l('PublishDate'),
data: "publishDate",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString();
}
},
{
title: l('Price'),
data: "price"
},
{
title: l('CreationTime'),
data: "creationTime",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString(luxon.DateTime.DATETIME_SHORT);
}
}
]
})
);
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
```
* We've used the [Datatables](https://datatables.net/) to list our books.
* Also defined **create** and **update** modals by using [ABP Modal Manager](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Modals#modalmanager-reference), but we didn't create them yet, so let's create the modals.
First, create a **CreateModal** razor page and update the **CreateModal.cshtml** and **CreateModal.cshtml.cs** files as below:
**CreateModal.cshtml**
```html
@page
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model CreateModalModel
@{
Layout = null;
}
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
<abp-modal>
<abp-modal-header title="New Book"></abp-modal-header>
<abp-modal-body>
<abp-form-content />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</abp-dynamic-form>
```
* We've used `abp-dynamic-form` tag-helper and passed it a `Book` model, this tag helper will simply create form contents (inputs, select boxes etc.) on behalf of us.
* **CreateModal.cshtml.cs**
```csharp
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books;
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public CreateModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public void OnGet()
{
Book = new CreateBookDto();
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.CreateAsync(Book);
return NoContent();
}
}
```
* In this file, we simply define **CreateBookDto** as a bind property and we'll use this class's properties in the form. Thanks to the `abp-dynamic-form` tag-helper we don't need to define all of these form elements one by one, it will generate on behalf of us.
We can create an **EditModal** razor page and update the **EditModal.cshtml** and **EditModal.cshtml.cs** files as below:
**EditModal.cshtml**
```html
@page
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@{
Layout = null;
}
<form asp-page="/Books/EditModal">
<abp-modal>
<abp-modal-header title="Update"></abp-modal-header>
<abp-modal-body>
<abp-input asp-for="Id"/>
<abp-input asp-for="Book.Name"/>
<abp-input asp-for="Book.Price"/>
<abp-select asp-for="Book.Type"/>
<abp-input asp-for="Book.PublishDate"/>
<abp-input asp-for="Book.ConcurrencyStamp" type="hidden"/>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</form>
```
* Here, we didn't use the `abp-dynamic-form` tag-helper and added all the necessary form elements to our form one by one.
* As you may have noticed, we've set the input type as **hidden** for the **ConcurrencyStamp** input, because the end-user should not see this value.
> Instead of doing it like that, we could create a view model class and use the `[HiddenInput]` data attribute for the **ConcurrencyStamp** property and use the `abp-dynamic-form` tag-helper. But to simplify the article I didn't want to do that, if you want you can create a view model and define the necessary data attributes for properties.
**EditModal.cshtml.cs**
```csharp
public class EditModalModel : BookStorePageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[BindProperty]
public UpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map<BookDto, UpdateBookDto>(bookDto);
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.UpdateAsync(Id, Book);
return NoContent();
}
}
```
Lastly, we can define the necessary mapping configurations and run the application to see the result.
Open the `BookStoreWebAutoMapperProfile.cs` class and update the content as below:
```csharp
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, UpdateBookDto>();
}
}
```
Then we can run the application, navigate to the **/Books** endpoint and see the result.
![](concurrency-mismatch.gif)
* In the image above, we can see that multiple users open the edit model to change a record and try to update the relevant record independently of each other.
* After the first user updated the record, the second user tries to update the same record without getting the last state of the record. And therefore `AbpDbConcurrencyException` is thrown because **ConcurrencyStamp** values are different from each other.
* The second user should close and re-open the model to get the last state of the record and then they can make changes to the current record.

BIN
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

113
docs/en/Community-Articles/2022-04-14-Dependency-Injection/POST.md

@ -0,0 +1,113 @@
# Dealing with Multiple Implementations of a Service in ASP.NET Core & ABP Dependency Injection
ASP.NET Core provides a built-in [dependency injection system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) to register your services to the dependency injection container and inject/resolve them whenever you need. ABP's [dependency injection infrastructure](https://docs.abp.io/en/abp/latest/Dependency-Injection) is built on ASP.NET Core's DI system, automates service registration by conventions and provides some additional features.
In this tutorial, I will explain how you can register multiple implementations of the same service interface and inject/resolve all these implementations when you need them.
## Defining Services
Assume that you have an `IExternalLogger` interface with two implementations:
````csharp
public interface IExternalLogger
{
Task LogAsync(string logText);
}
public class ElasticsearchExternalLogger : IExternalLogger
{
public async Task LogAsync(string logText)
{
// TODO...
}
}
public class AzureExternalLogger : IExternalLogger
{
public async Task LogAsync(string logText)
{
// TODO...
}
}
````
An example service injecting the `IExternalLogger` interface:
````csharp
public class MyService : ITransientDependency
{
private readonly IExternalLogger _externalLogger;
public MyService(IExternalLogger externalLogger)
{
_externalLogger = externalLogger;
}
public async Task DemoAsync()
{
await _externalLogger.LogAsync("Example log message...");
}
}
````
In this example, we haven't registered any of the implementation classes to the dependency injection system yet. So, if we try to use the `MyService` class, we get an error indicating that no implementation found for the `IExternalLogger` service. We should register at least one implementation for the `IExternalLogger` interface.
## Registering Services
If we register both of the `ElasticsearchExternalLogger` and `AzureExternalLogger` services for the `IExternalLogger` interface, and then try to inject the `IExternalLogger` interface, the last registered implementation will be used. However, how to determine the last registered implementation?
If we implement one of the [dependency interfaces](https://docs.abp.io/en/abp/latest/Dependency-Injection#dependency-interfaces) (e.g. `ITransientDependency`), then the registration order will be uncertain (it may depend on the namespaces of the classes). The *last registered implementation* can be different than you expect. So, it is not suggested to use the dependency interfaces to register multiple implementations.
You can register your services in the `ConfigureServices` method of your module:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<IExternalLogger, ElasticsearchExternalLogger>();
context.Services.AddTransient<IExternalLogger, AzureExternalLogger>();
}
````
In this case, you get an `AzureExternalLogger` instance when you inject the `IExternalLogger` interface, because the last registered implementation is the `AzureExternalLogger` class.
## Injecting Multiple Implementations
When you have multiple implementation of an interface, you may want to work with all these implementations. For this example, you may want to write log to all the external loggers. We can change the `MyService` implementation as the following:
````csharp
public class MyService : ITransientDependency
{
private readonly IEnumerable<IExternalLogger> _externalLoggers;
public MyService(IEnumerable<IExternalLogger> externalLoggers)
{
_externalLoggers = externalLoggers;
}
public async Task DemoAsync()
{
foreach (var externalLogger in _externalLoggers)
{
await externalLogger.LogAsync("Example log message...");
}
}
}
````
In this example, we are injecting `IEnumerable<IExternalLogger>` instead of `IExternalLogger`, so we have a collection of the `IExternalLogger` implementations. Then we are using a `foreach` loop to write the same log text to all the `IExternalLogger` implementations.
If you are using `IServiceProvider` to resolve dependencies, then use its `GetServices` method to obtain a collection of the service implementations:
````csharp
IEnumerable<IExternalLogger> services = _serviceProvider.GetServices<IExternalLogger>();
````
## Further Reading
In this small tutorial, I explained how you can register multiple implementations of the same interface to the dependency injection system and inject/resolve all of them when you need.
If you want to get more information about ABP's and ASP.NET Core's dependency injection systems, you can read the following documents:
* [ABP's Dependency Injection documentation](https://docs.abp.io/en/abp/latest/Dependency-Injection)
* [ASP.NET Core Dependency Injection best practices, tips & tricks](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96)
* [ASP.NET Core's Dependency Injection documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection)

146
docs/en/Concurrency-Check.md

@ -1,3 +1,147 @@
## Concurrency Check
TODO
### Introduction
Concurrency Check (also known as **Concurrency Control**) refers to specific mechanisms used to ensure data consistency in the presence of concurrent changes (multiple processes, users access or change the same data in a database at the same time).
There are two commonly used concurrency control mechanisms/approaches:
* **Optimistic Concurrency Control**: Optimistic Concurrency Control allows multiple users to attempt to **update** the same record without informing the users that others are also attempting to **update** it.
* If a user successfully updates the record, the other users need to get the latest changes for the current record to be able to make changes.
* ABP's concurrency check system uses the **Optimistic Concurrency Control**.
* **Pessimistic Concurrency Control**: Pessimistic Concurrency Control prevents simultaneous updates to records and uses a locking mechanism. For more information please see [here](https://www.martinfowler.com/eaaCatalog/pessimisticOfflineLock.html).
### Usage
#### `IHasConcurrencyStamp` Interface
To enable **concurrency control** to your entity class, you should implement the `IHasConcurrencyStamp` interface, directly or indirectly.
```csharp
public interface IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
}
```
* It is the base interface for **concurrency control** and only has a simple property named `ConcurrencyStamp`.
* While a new record is **creating**, if the entity implements the `IHasConcurrencyStamp` interface, ABP Framework automatically sets a unique value to the **ConcurrencyStamp** property.
* While a record is **updating**, ABP Framework compares the **ConcurrencyStamp** property of the entity with the provided **ConcurrencyStamp** value by the user and if the values match, it automatically updates the **ConcurrencyStamp** property with the new unique value. If there is a mismatch, `AbpDbConcurrencyException` is thrown.
**Example: Applying Concurrency Control for the Book Entity**
Implement the `IHasConcurrencyStamp` interface for your entity:
```csharp
public class Book : Entity<Guid>, IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
//...
}
```
Also, implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface:
```csharp
public class BookDto : EntityDto<Guid>, IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
public class UpdateBookDto : IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
```
Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below:
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
//...
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await BookRepository.GetAsync(id);
book.ConcurrencyStamp = input.ConcurrencyStamp;
//set other input values to the entity ...
await BookRepository.UpdateAsync(book);
}
}
```
* After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown.
#### Base Classes
[Aggregate Root](./Entities.md#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface. So, if you are deriving from one of these base classes, you don't need to manually implement the `IHasConcurrencyStamp` interface:
- `AggregateRoot`, `AggregateRoot<TKey>`
- `CreationAuditedAggregateRoot`, `CreationAuditedAggregateRoot<TKey>`
- `AuditedAggregateRoot`, `AuditedAggregateRoot<TKey>`
- `FullAuditedAggregateRoot`, `FullAuditedAggregateRoot<TKey>`
**Example: Applying Concurrency Control for the Book Entity**
You can inherit your entity from one of [the base classes](#base-classes):
```csharp
public class Book : FullAuditedAggregateRoot<Guid>
{
//...
}
```
Then, you can implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface:
```csharp
public class BookDto : EntityDto<Guid>, IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
public class UpdateBookDto : IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
```
Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below:
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
//...
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await BookRepository.GetAsync(id);
book.ConcurrencyStamp = input.ConcurrencyStamp;
//set other input values to the entity ...
await BookRepository.UpdateAsync(book);
}
}
```
After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. You can either handle the exception manually or let the ABP Framework handle it for you.
ABP Framework shows a user-friendly error message as in the image below, if you don't handle the exception manually.
![Optimistic Concurrency](./images/optimistic-concurrency.png)

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

@ -109,68 +109,27 @@ public class MyLocalIdentityUserChangeEventHandler :
[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server.
Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below:
Assume that you want to get informed when `Tenant` entity (of the [Tenant Management](Modules/Tenant-Management.md) module) has created. In this case, you can subscribe to the `EntityCreatedEto<TenantEto>` event as shown in the following example:
````csharp
public class MyDistributedIdentityUserChangeEventHandler :
IDistributedEventHandler<EntityCreatedEto<EntityEto>>,
IDistributedEventHandler<EntityUpdatedEto<EntityEto>>,
IDistributedEventHandler<EntityDeletedEto<EntityEto>>,
public class MyDistributedEventHandler :
IDistributedEventHandler<EntityCreatedEto<TenantEto>>,
ITransientDependency
{
public async Task HandleEventAsync(EntityCreatedEto<EntityEto> eventData)
public async Task HandleEventAsync(EntityCreatedEto<TenantEto> eventData)
{
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
{
var userId = Guid.Parse(eventData.Entity.KeysAsString);
//...handle the "created" event
}
}
public async Task HandleEventAsync(EntityUpdatedEto<EntityEto> eventData)
{
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
{
var userId = Guid.Parse(eventData.Entity.KeysAsString);
//...handle the "updated" event
}
}
public async Task HandleEventAsync(EntityDeletedEto<EntityEto> eventData)
{
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser")
{
var userId = Guid.Parse(eventData.Entity.KeysAsString);
//...handle the "deleted" event
}
}
}
````
* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus.
* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity).
Pre-built application modules do not define specialized event types yet (like `UserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example:
````csharp
public class MyDistributedIdentityUserCreatedEventHandler :
IDistributedEventHandler<EntityCreatedEto<UserEto>>,
ITransientDependency
{
public async Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
var userId = eventData.Entity.Id;
var userName = eventData.Entity.UserName;
//...handle the "created" event
var tenantId = eventData.Entity.Id;
var tenantName = eventData.Entity.Name;
//...your custom logic
}
//...
}
````
* This handler is executed only when a new user has been created.
This handler is executed only when a new tenant has been created. All the pre-built ABP [application modules](Modules/Index.md) define corresponding `ETO` types for their entities. So, you can easily get informed when they changes.
> The only pre-defined specialized event class is the `UserEto`. For example, you can subscribe to the `EntityCreatedEto<UserEto>` to get notified when a user has created. This event also works for the Identity module.
> Notice that ABP doesn't publish distributed events for an entity by default. Because it has a cost and should be enabled by intention. See the [distributed event bus document](Distributed-Event-Bus.md) to learn more.
## See Also

2
docs/en/Customizing-Application-Modules-Guide.md

@ -31,7 +31,7 @@ If you don't think to make huge changes on the pre-built modules, re-using them
If you want to make **huge changes** or add **major features** on a pre-built module, but the available extension points are not enough, you can consider to directly work the source code of the depended module.
In this case, you typically **add the source code** of the module to your solution and replace **every** package reference in the solution with its corresponding local project references. **[ABP CLI](CLI.md)** automates this process for you.
In this case, you typically **add the source code** of the module to your solution and replace **every** package reference in the solution with its corresponding local project references. **[ABP CLI](CLI.md)**'s `add-module` command automates this process for you with the `--with-source-code` parameter. This command can also replace a module by its source code if the module already installed as NuGet packages.
#### Separating the Module Solution

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

@ -161,6 +161,73 @@ This example class inherits from the `IdentityUserManager` [domain service](Doma
Check the [localization system](Localization.md) to learn how to localize the error messages.
### Example: Overriding a Repository
````csharp
public class MyEfCoreIdentityUserRepository : EfCoreIdentityUserRepository
{
public MyEfCoreIdentityUserRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
/* You can override any base method here */
}
````
In this example, we are overriding the `EfCoreIdentityUserRepository` class that is defined by the [Identity module](Modules/Identity.md). This is the [Entity Framework Core](Entity-Framework-Core.md) implementation of the user repository.
Thanks to the naming convention (`MyEfCoreIdentityUserRepository` ends with `EfCoreIdentityUserRepository`), no additional setup is required. You can override any base method to customize it for your needs.
However, if you inject `IRepository<IdentityUser>` or `IRepository<IdentityUser, Guid>`, it will still use the default repository implementation. To replace the default repository implementation, write the following code in the `ConfigureServices` method of your module class:
````csharp
context.Services.AddDefaultRepository(
typeof(Volo.Abp.Identity.IdentityUser),
typeof(MyEfCoreIdentityUserRepository),
replaceExisting: true
);
````
In this way, your implementation will be used if you inject `IRepository<IdentityUser>`, `IRepository<IdentityUser, Guid>` or `IIdentityUserRepository`.
If you want to add extra methods to your repository and use it in your own code, you can define an interface and expose it from your repository implementation. You can also extend the pre-built repository interface. Example:
````csharp
public interface IMyIdentityUserRepository : IIdentityUserRepository
{
public Task DeleteByEmailAddress(string email);
}
````
The `IMyIdentityUserRepository` interface extends the Identity module's `IIdentityUserRepository` interface. Then you can implement it as shown in the following example:
````csharp
[ExposeServices(typeof(IMyIdentityUserRepository), IncludeDefaults = true)]
public class MyEfCoreIdentityUserRepository
: EfCoreIdentityUserRepository, IMyIdentityUserRepository
{
public MyEfCoreIdentityUserRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task DeleteByEmailAddress(string email)
{
var dbContext = await GetDbContextAsync();
var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == email);
if (user != null)
{
dbContext.Users.Remove(user);
}
}
}
````
The `MyEfCoreIdentityUserRepository` class implements the `IMyIdentityUserRepository` interface. `ExposeServices` attribute is needed since ABP can not expose `IMyIdentityUserRepository` by naming conventions (`MyEfCoreIdentityUserRepository` doesn't end with `MyIdentityUserRepository`). Now, you can inject the `IMyIdentityUserRepository` interface into your services and call its `DeleteByEmailAddress` method.
### Example: Overriding a Controller
````csharp
@ -205,7 +272,8 @@ If you don't want to remove either controller, you can configure `AbpAspNetCoreM
```csharp
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(MyAccountController));
options.IgnoredControllersOnModelExclusion
.AddIfNotContains(typeof(MyAccountController));
});
```

98
docs/en/Dependency-Injection.md

@ -49,7 +49,7 @@ public class BlogModule : AbpModule
}
````
The section below explains the conventions and configurations.
The sections below explain the conventions and configurations.
### Inherently Registered Types
@ -266,6 +266,102 @@ public class MyService : ITransientDependency
}
````
### Dealing with multiple implementations
You can register multiple implementations of the same service interface. Assume that you have an `IExternalLogger` interface with two implementations:
````csharp
public interface IExternalLogger
{
Task LogAsync(string logText);
}
public class ElasticsearchExternalLogger : IExternalLogger
{
public async Task LogAsync(string logText)
{
//TODO...
}
}
public class AzureExternalLogger : IExternalLogger
{
public Task LogAsync(string logText)
{
throw new System.NotImplementedException();
}
}
````
In this example, we haven't registered any of the implementation classes to the dependency injection system yet. So, if we try to inject the `IExternalLogger` interface, we get an error indicating that no implementation found.
If we register both of the `ElasticsearchExternalLogger` and `AzureExternalLogger` services for the `IExternalLogger` interface, and then try to inject the `IExternalLogger` interface, then the last registered implementation will be used.
An example service injecting the `IExternalLogger` interface:
````csharp
public class MyService : ITransientDependency
{
private readonly IExternalLogger _externalLogger;
public MyService(IExternalLogger externalLogger)
{
_externalLogger = externalLogger;
}
public async Task DemoAsync()
{
await _externalLogger.LogAsync("Example log message...");
}
}
````
Here, as said before, we get the last registered implementation. However, how to determine the last registered implementation?
If we implement one of the dependency interfaces (e.g. `ITransientDependency`), then the registration order will be uncertain (it may depend on the namespaces of the classes). The *last registered implementation* can be different than you expect. So, it is not suggested to use the dependency interfaces to register multiple implementations.
You can register your services in the `ConfigureServices` method of your module:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<IExternalLogger, ElasticsearchExternalLogger>();
context.Services.AddTransient<IExternalLogger, AzureExternalLogger>();
}
````
In this case, you get an `AzureExternalLogger` instance when you inject the `IExternalLogger` interface, because the last registered implementation is the `AzureExternalLogger` class.
When you have multiple implementation of an interface, you may want to work with all these implementations. Assume that you want to write log to all the external loggers. We can change the `MyService` implementation as the following:
````csharp
public class MyService : ITransientDependency
{
private readonly IEnumerable<IExternalLogger> _externalLoggers;
public MyService(IEnumerable<IExternalLogger> externalLoggers)
{
_externalLoggers = externalLoggers;
}
public async Task DemoAsync()
{
foreach (var externalLogger in _externalLoggers)
{
await externalLogger.LogAsync("Example log message...");
}
}
}
````
In this example, we are injecting `IEnumerable<IExternalLogger>` instead of `IExternalLogger`, so we have a collection of the `IExternalLogger` implementations. Then we are using a `foreach` loop to write the same log text to all the `IExternalLogger` implementations.
If you are using `IServiceProvider` to resolve dependencies, then use its `GetServices` method to obtain a collection of the service implementations:
````csharp
IEnumerable<IExternalLogger> services = _serviceProvider.GetServices<IExternalLogger>();
````
### Releasing/Disposing Services
If you used a constructor or property injection, you don't need to be concerned about releasing the service's resources. However, if you have resolved a service from ``IServiceProvider``, you might, in some cases, need to take care about releasing the service resources.

99
docs/en/Deployment/Clustered-Environment.md

@ -0,0 +1,99 @@
# Deploying to a Clustered Environment
This document introduces the topics that you should consider when you are deploying your application to a clustered environment where **multiple instances of your application run concurrently**, and explains how you can deal with these topics in your ABP based application.
> This document is valid regardless you have a monolith application or a microservice solution. The Application term is used for a process. An application can be a monolith web application, a service in a microservice solution, a console application, or another kind of an executable process.
>
> For example, if you are deploying your application to Kubernetes and configure your application or service to run in multiple pods, then your application or service runs in a clustered environment.
## Understanding the Clustered Environment
> You can skip this section if you are already familiar with clustered deployment and load balancers.
### Single Instance Deployment
Consider an application deployed as a **single instance**, as illustrated in the following figure:
![deployment-single-instance](../images/deployment-single-instance.png)
Browsers and other client applications can directly make HTTP requests to your application. You can put a web server (e.g. IIS or NGINX) between the clients and your application, but you still have a single application instance running in a single server or container. Single-instance configuration is **limited to scale** since it runs in a single server and you are limited with the server's capacity.
### Clustered Deployment
**Clustered deployment** is the way of running **multiple instances** of your application **concurrently** in a single or multiple servers. In this way, different instances can serve different requests and you can scale by adding new servers to the system. The following figure shows a typical implementation of clustering using a **load balancer**:
![deployment-clustered](../images/deployment-clustered.png)
### Load Balancers
[Load balancers](https://en.wikipedia.org/wiki/Load_balancing_(computing)) have a lot of features, but they fundamentally **forward an incoming HTTP request** to an instance of your application and return your response back to the client application.
Load balancers can use different algorithms for selecting the application instance while determining the application instance that is used to deliver the incoming request. **Round Robin** is one of the simplest and most used algorithms. Requests are delivered to the application instances in rotation. First instance gets the first request, second instance gets the second, and so on. It returns to the first instance after all the instances are used, and the algorithm goes like that for the next requests.
### Potential Problems
Once multiple instances of your application run in parallel, you should carefully consider the following topics:
* Any **state (data) stored in memory** of your application will become a problem when you have multiple instances. A state stored in memory of an application instance may not be available in the next request since the next request will be handled by a different application instance. While there are some solutions (like sticky sessions) to overcome this problem user-basis, it is a **best practice to design your application as stateless** if you want to run it in a cluster, container or/and cloud.
* **In-memory caching** is a kind of in-memory state and should not be used in a clustered application. You should use **distributed caching** instead.
* You shouldn't store data in the **local file system**. It should be available to all instances of your application. Different application instance may run in different containers or servers and they may not be able to have access to the same file system. You can use a **cloud or external storage provider** as a solution.
* If you have **background workers** or **job queue managers**, you should be careful since multiple instances may try to execute the same job or perform the same work concurrently. As a result, you may have the same work done multiple times or you may get a lot of errors while trying to access and change the same resources.
You may have more problems with clustered deployment, but these are the most common ones. ABP has been designed to be compatible with the clustered deployment scenario. The following sections explain what you should do when you are deploying your ABP based application to a clustered environment.
## Switching to a Distributed Cache
ASP.NET Core provides different kind of caching features. [In-memory cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory) stores your objects in the memory of the local server and is only available to the application that stored the object. Non-sticky sessions in a clustered environment should use the [distributed caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) except some specific scenarios (for example, you can cache a local CSS file into memory. It is read-only data and it is the same in all application instances. You can cache it in memory for performance reasons without any problem).
[ABP's Distributed Cache](../Caching.md) extends [ASP.NET Core's distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) infrastructure. It works in-memory by default. You should configure an actual distributed cache provider when you want to deploy your application to a clustered environment.
> You should configure the cache provider for clustered deployment, even if your application doesn't directly use `IDistributedCache`. Because the ABP Framework and the pre-built [application modules](../Modules/Index.md) are using distributed cache.
ASP.NET Core provides multiple integrations to use as your distributed cache provider, like [Redis](https://redis.io/) and [NCache](https://www.alachisoft.com/ncache/). You can follow [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to learn how to use them in your applications.
If you decided to use Redis as your distributed cache provider, **follow [ABP's Redis Cache Integration document](../Redis-Cache.md)** for the steps you need to follow to install it into your application and setup your Redis configuration.
> Based on your preferences while creating a new ABP solution, Redis cache might be pre-installed in your solution. For example, if you have selected the *Tiered* option with the MVC UI, Redis cache comes as pre-installed. Because, in this case, you have two applications in your solution and they should use the same cache source to be consistent.
## Using a Proper BLOB Storage Provider
If you have used ABP's [BLOB Storing](../Blob-Storing.md) feature with the [File System provider](../Blob-Storing-File-System.md), you should use another provider in your clustered environment since the File System provider uses the application's local file system.
The [Database BLOB provider](../Blob-Storing-Database) is the easiest way since it uses your application's main database (or another database if you configure) to store BLOBs. However, you should remember that BLOBs are large objects and may quickly increase your database's size.
> [ABP Commercial](https://commercial.abp.io/) startup solution templates come with the database BLOB provider as pre-installed, and stores BLOBs in the application's database.
Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOG storage providers.
## Configuring Background Jobs
ABP's [background job system](../Background-Jobs.md) is used to queue tasks to be executed in the background. Background job queue is persistent and a queued task is guaranteed to be executed (it is re-tried if it fails).
ABP's default background job manager is compatible with clustered environments. It uses a [distributed lock](../Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time. See the *Configuring a Distributed Lock Provider* section below to learn how to configure a distributed lock provider for your application, so the default background job manager properly works in a clustered environment.
If you don't want to use a distributed lock provider, you may go with the following options:
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs).
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance.
> If you are using an external background job integration (e.g. [Hangfire](../Background-Workers-Hangfire.md) or [Quartz](../Background-Workers-Quartz.md)) instead of the default background job manager, then please refer to your provider's documentation to learn how it should be configured for a clustered environment.
## Configuring a Distributed Lock Provider
ABP provides a distributed locking abstraction with an implementation made with the [DistributedLock](https://github.com/madelson/DistributedLock) library. A distributed lock is used to control concurrent access to a shared resource by multiple applications to prevent corruption of the resource because of concurrent writes. The ABP Framework and some pre-built [application modules](../Modules/Index.md) are using distributed locking for several reasons.
However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, please follow the [distributed lock](../Distributed-Locking.md) document to configure a provider for your application, if it is not already configured.
## Implementing Background Workers
ASP.NET Core provides [hosted services](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) and ABP provides [background workers](../Background-Workers.md) to perform tasks in background threads in your application.
If your application has tasks running in the background, you should consider how they will behave in a clustered environment, especially if your background tasks are using the same resources. You should design your background tasks so that they continue to work properly in the clustered environment.
Assume that your background worker in your SaaS application checks user subscriptions and sends emails if their subscription renewal date approaches. If the background task runs in multiple application instances, it is probable to send the same email many times to some users, which will disturb them.
We suggest you to use one of the following approaches to overcome the problem:
* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](../Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed.
* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers.
* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance.

9
docs/en/Deployment/Index.md

@ -0,0 +1,9 @@
# Deployment
Deploying an ABP application is not different than deploying any .NET or ASP.NET Core application. You can deploy it to a cloud provider (e.g. Azure, AWS, Google Could) or on-premise server, IIS or any other web server. ABP's documentation doesn't contain much information on deployment. You can refer to your provider's documentation.
However, there are some topics that you should care about when you are deploying your applications. Most of them are general software deployment considerations, but you should understand how to handle them within your ABP based applications. We've prepared guides for this purpose and we suggest you to read these guides carefully before designing your deployment configuration.
## Guides
* [Deploying to a clustered environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently.

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

@ -228,6 +228,7 @@ namespace AbpDemo
````
* `MyHandler` implements the `IDistributedEventHandler<EntityUpdatedEto<ProductEto>>`.
* It is required to register your handler class to the [dependency injection](Dependency-Injection.md) system. Implementing `ITransient` like in this example is an easy way.
### Configuration
@ -242,10 +243,10 @@ Configure<AbpDistributedEntityEventOptions>(options =>
options.AutoEventSelectors.AddAll();
//Enable for a single entity
options.AutoEventSelectors.Add<IdentityUser>();
options.AutoEventSelectors.Add<Product>();
//Enable for all entities in a namespace (and child namespaces)
options.AutoEventSelectors.AddNamespace("Volo.Abp.Identity");
options.AutoEventSelectors.AddNamespace("MyProject.Products");
//Custom predicate expression that should return true to select a type
options.AutoEventSelectors.Add(
@ -265,10 +266,21 @@ Once you enable **auto events** for an entity, ABP Framework starts to publish e
* `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.
So, you can implement the `IDistributedEventHandler<EntityUpdatedEto<EntityEto>>` to subscribe the update events. However, it is not a good approach to subscribe to such a generic event, because you handle the update events for all entities in a single handler (since they all use the same ETO object). You can define the corresponding ETO type for the entity type.
**Example: Declare to use `ProductEto` for the `Product` entity**
````csharp
public class ProductEto
{
public Guid Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
}
````
Then you can use the `AbpDistributedEntityEventOptions.EtoMappings` option to map your `Product` entity to the `ProductEto`:
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
@ -282,24 +294,4 @@ 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.
> 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 object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it.

110
docs/en/Distributed-Locking.md

@ -0,0 +1,110 @@
# Distributed Locking
Distributed locking is a technique to manage many applications that try to access the same resource. The main purpose is to allow only one of many applications to access the same resource at the same time. Otherwise, accessing the same object from various applications may corrupt the value of the resources.
> ABP's current distributed locking implementation is based on the [DistributedLock](https://github.com/madelson/DistributedLock) library.
## Installation
You can open a command-line terminal and type the following command to install the [Volo.Abp.DistributedLocking](https://www.nuget.org/packages/Volo.Abp.DistributedLocking) package into your project:
````bash
abp add-package Volo.Abp.DistributedLocking
````
This package provides the necessary API to use the distributed locking system, however, you should configure a provider before using it.
### Configuring a Provider
The [DistributedLock](https://github.com/madelson/DistributedLock) library provides [various of implementations](https://github.com/madelson/DistributedLock#implementations) for the locking, like [Redis](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md) and [ZooKeeper](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.ZooKeeper.md).
For example, if you want to use the [Redis provider](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md), you should add [DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet package to your project, then add the following code into the `ConfigureServices` method of your ABP [module](Module-Development-Basics.md) class:
````csharp
using Medallion.Threading;
using Medallion.Threading.Redis;
namespace AbpDemo
{
[DependsOn(
typeof(AbpDistributedLockingModule)
//If you have the other dependencies, you should do here
)]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer
.Connect(configuration["Redis:Configuration"]);
return new
RedisDistributedSynchronizationProvider(connection.GetDatabase());
});
}
}
}
````
This code gets the Redis connection string from the [configuration](Configuration.md), so you can add the following lines to your `appsettings.json` file:
````json
"Redis": {
"Configuration": "127.0.0.1"
}
````
## Usage
There are two ways to use the distributed locking API: ABP's `IAbpDistributedLock` abstraction and [DistributedLock](https://github.com/madelson/DistributedLock) library's API.
### Using the IAbpDistributedLock Service
`IAbpDistributedLock` is a simple service provided by the ABP framework for simple usage of distributed locking.
**Example: Using the `IAbpDistributedLock.TryAcquireAsync` method**
````csharp
using Volo.Abp.DistributedLocking;
namespace AbpDemo
{
public class MyService : ITransientDependency
{
private readonly IAbpDistributedLock _distributedLock;
public MyService(IAbpDistributedLock distributedLock)
{
_distributedLock = distributedLock;
}
public async Task MyMethodAsync()
{
await using (var handle =
await _distributedLock.TryAcquireAsync("MyLockName"))
{
if (handle != null)
{
// your code that access the shared resource
}
}
}
}
}
````
`TryAcquireAsync` may not acquire the lock. It returns `null` if the lock could not be acquired. In this case, you shouldn't access the resource. If the handle is not `null`, it means that you've obtained the lock and can safely access the resource.
`TryAcquireAsync` method gets the following parameters:
* `name` (`string`, required): Unique name of your lock. Different named locks are used to access different resources.
* `timeout` (`TimeSpan`): A timeout value to wait to obtain the lock. Default value is `TimeSpan.Zero`, which means it doesn't wait if the lock is already owned by another application.
* `cancellationToken`: A cancellation token that can be triggered later to cancel the operation.
### Using DistributedLock Library's API
ABP's `IAbpDistributedLock` service is very limited and mainly designed to be internally used by the ABP Framework. For your own applications, you can use the DistributedLock library's own API. See its [own documentation](https://github.com/madelson/DistributedLock) for details.
## The Volo.Abp.DistributedLocking.Abstractions Package
If you are building a reusable library or an application module, then you may not want to bring an additional dependency to your module for simple applications that run as a single instance. In this case, your library can depend on the [Volo.Abp.DistributedLocking.Abstractions](https://nuget.org/packages/Volo.Abp.DistributedLocking.Abstractions) package which defines the `IAbpDistributedLock` service and implements it as in-process (not distributed actually). In this way, your library can run properly (without a distributed lock provider dependency) in an application that runs as a single instance. If the application is deployed to a [clustered environment](Deployment/Clustered-Environment.md), then the application developer should install a real distributed provider as explained in the *Installation* section.

4
docs/en/Entity-Framework-Core-MySQL.md

@ -1,5 +1,7 @@
# Switch to EF Core MySQL Provider
> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with MySQL. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to MySQL after creating the solution.
This document explains how to switch to the **MySQL** 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
@ -15,7 +17,7 @@ Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFra
Find `UseSqlServer()` calls in your solution. Check the following files:
* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`.
* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.FromString("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`)
* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.Parse("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`)
> Depending on your solution structure, you may find more code files need to be changed.

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

@ -1,5 +1,7 @@
# Switch to EF Core Oracle Provider
> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with Oracle. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to Oracle after creating the solution.
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.
ABP Framework provides integrations for two different Oracle packages. See one of the following documents based on your provider decision:

23
docs/en/Entity-Framework-Core-Other-DBMS.md

@ -1,15 +1,32 @@
# Switch to Another DBMS for Entity Framework Core
**[The application startup template](Startup-Templates/Application.md)** comes with **SQL Server provider pre-configured** for the Entity Framework Core. However, EF Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them within your ABP based applications.
[ABP CLI](CLI.md) provides a `-dbms` option to allow you to choose your Database Management System (DBMS) while creating a new solution. It accepts the following values:
ABP framework provides **integration packages** for some common DBMSs to make the configuration a bit easier. You can use the following documents to learn how to **switch to your favorite DBMS**:
- `SqlServer` (default)
- `MySQL`
- `SQLite`
- `Oracle`
- `Oracle-Devart`
- `PostgreSQL`
So, if you want to use MySQL for your solution, you can use the `-dbms MySQL` option while using the `abp new` command. Example:
````bash
abp new BookStore -dbms MySQL
````
Also, the [Get Started page](https://abp.io/get-started) on the ABP website allows you to select one of the providers.
> **This document provides guidance for who wants to manually change their DBMS after creating the solution.**
You can use the following documents to learn how to **switch to your favorite DBMS**:
* [MySQL](Entity-Framework-Core-MySQL.md)
* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
* [Oracle](Entity-Framework-Core-Oracle.md)
* [SQLite](Entity-Framework-Core-SQLite.md)
However, you can configure your DBMS provider **without** these integration packages. While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider.
You can also configure your DBMS provider **without** these integration packages. While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it yourself if there is no integration package for your DBMS provider.
For an example, this document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md).

2
docs/en/Entity-Framework-Core-PostgreSQL.md

@ -1,5 +1,7 @@
# Switch to EF Core PostgreSQL Provider
> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with PostgreSQL. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to PostgreSQL after creating the solution.
This document explains how to switch to the **PostgreSQL** 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

2
docs/en/Entity-Framework-Core-SQLite.md

@ -1,5 +1,7 @@
# Switch to EF Core SQLite Provider
> [ABP CLI](CLI.md) and the [Get Started](https://abp.io/get-started) page already provides an option to create a new solution with SQLite. See [that document](Entity-Framework-Core-Other-DBMS.md) to learn how to use. This document provides guidance for who wants to manually switch to SQLite after creating the solution.
This document explains how to switch to the **SQLite** 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

4
docs/en/Features.md

@ -308,7 +308,7 @@ The example above defines a *Reporting* feature with two children: *PDF Reportin
### Changing Features Definitions of a Depended Module
A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing feature definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
**Example: Manipulate an existing feature definition**
@ -437,4 +437,4 @@ Use this code inside the `ConfigureServices` of your [module](Module-Development
### Feature Store
`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information
`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information

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

@ -77,20 +77,17 @@ using BasicAspNetCoreApplication;
var builder = WebApplication.CreateBuilder(args);
builder.Services.ReplaceConfiguration(builder.Configuration);
builder.Services.AddApplication<AppModule>();
await builder.Services.AddApplicationAsync<AppModule>();
var app = builder.Build();
app.InitializeApplication();
app.Run();
await app.InitializeApplicationAsync();
await app.RunAsync();
````
``builder.Services.AddApplication<AppModule>();`` adds all services defined in all modules starting from the ``AppModule``.
``builder.Services.AddApplicationAsync<AppModule>();`` adds all services defined in all modules starting from the ``AppModule``.
``app.InitializeApplication()`` initializes and starts the application.
``app.InitializeApplicationAsync()`` initializes and starts the application.
## Run the Application!
@ -98,9 +95,9 @@ That's all! Run the application, it will just work as expected.
## Using Autofac as the Dependency Injection Framework
While AspNet Core's Dependency Injection (DI) system is fine for basic requirements, [Autofac](https://autofac.org/) provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features.
While ASP.NET Core's Dependency Injection (DI) system is fine for basic requirements, [Autofac](https://autofac.org/) provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features.
Replacing AspNet Core's DI system by Autofac and integrating to ABP is pretty easy.
Replacing ASP.NET Core's DI system by Autofac and integrating to ABP is pretty easy.
1. Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) package
@ -128,19 +125,14 @@ var builder = WebApplication.CreateBuilder(args);
builder.Host.UseAutofac(); //Add this line
builder.Services.ReplaceConfiguration(builder.Configuration);
builder.Services.AddApplication<AppModule>();
await builder.Services.AddApplicationAsync<AppModule>();
var app = builder.Build();
app.InitializeApplication();
app.Run();
await app.InitializeApplicationAsync();
await app.RunAsync();
````
## Source Code
Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication).

6
docs/en/Getting-Started-Create-Solution.md

@ -41,19 +41,19 @@ abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u
> [ABP CLI document](./CLI.md) covers all of the available commands and options.
## Mobile Development
### Mobile Development
If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends.
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.
### The Solution Structure
## The Solution Structure
The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details.
{{ if DB == "Mongo" }}
#### MongoDB Transactions
## MongoDB Transactions
The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable it in the *YourProjectMongoDbModule* class's `ConfigureServices` method:

4
docs/en/Getting-Started-Running-Solution.md

@ -91,10 +91,10 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project**
## Run the Application
> Please execute the `abp install-libs` command to restore the libs required by the web project before running the application.
{{ if UI == "MVC" || UI == "BlazorServer" }}
> Before starting the application, run `abp install-libs` command in your Web directory to restore the client-side libraries. This will populate the `libs` folder.
{{ if Tiered == "Yes" }}
> Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below.

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

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

4
docs/en/Migration-Guides/Abp-5-0-Blazor.md

@ -1,6 +1,6 @@
# ABP Blazor UI v4.x to v5.0 Migration Guide
> This document is for the ABP MVC / Razor Pages UI. See also [the main migration guide](Abp-5_0.md).
> This document is for the Blazor UI. See also [the main migration guide](Abp-5_0.md).
## Upgrading to the latest Blazorise
@ -16,4 +16,4 @@ Replace `AddBootstrapProviders()` with `AddBootstrap5Providers()`.
## Update Bundle
Use `abp bundle` command to update bundles if you are using Blazor WebAssembly.
Use `abp bundle` command to update bundles if you are using Blazor WebAssembly.

2
docs/en/Migration-Guides/Abp-5_0-Angular.md

@ -1,6 +1,6 @@
# Angular UI v4.x to v5.0 Migration Guide
This document is for the ABP MVC / Razor Pages UI. See also [the main migration guide](Abp-5_0.md).
> This document is for the Angular UI. See also [the main migration guide](Abp-5_0.md).
## Overall

2
docs/en/Migration-Guides/Abp-5_0.md

@ -117,5 +117,5 @@ db.AbpUsers.updateMany({},{$set:{ IsActive : true }})
## See Also
* [Angular UI 4.x to 5.0 Migration Guide](Abp-5_0-Angular.md)
* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide]()
* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide](Abp-5-0-MVC.md)
* [Blazor UI 4.x to 5.0 Migration Guide](Abp-5-0-Blazor.md)

52
docs/en/Migration-Guides/Abp-5_2.md

@ -0,0 +1,52 @@
# ABP Version 5.2 Migration Guide
This document is a guide for upgrading ABP v5.x solutions to ABP v5.2. Please read them all since v5.2 has some changes you should take care.
## MongoDB
- `IMongoDbRepositoryFilterer.AddGlobalFilters()` method is replaced with async one `IMongoDbRepositoryFilterer.AddGlobalFiltersAsync()`
## Blazor UI
If you use Blazor WASM or Blazor Server UI, you should follow this section.
### Blazorise 1.0
We've upgraded to Blazorise 1.0 stable version. So there is some breaking changes that you have to apply in your project.
Also You can review that pull request [#11649 - Blazorise 1.0 Migration](https://github.com/abpframework/abp/pull/11649)
- `NumericEdit` is now made around the native `input type="number"` so a lot of its formating features are moved to the new `NumericPicker` component. Replace NumericEdit with NumericPicker.
- Rename `DecimalsSeparator` to `DecimalSeparator` on the `DataGridColumn` and `NumericPicker`.
- Rename `MaxMessageSize` to `MaxChunkSize`.
- Remove `Fullscreen` parameter on `<ModalContent>` and replace it with `Size="ModalSize.Fullscreen"` parameter.
- Remove `NotificationType`, `Message`, and `Title` parameter from `<NotificationAlert>` component.
- Move `RightAligned` parameter from `<BarDropdownMenu>` to `<BarDropdown>` component.
- Rename any usage of the `ChangeTextOnKeyPress` parameter into `Immediate`.
- Rename any usage of `DelayTextOnKeyPress` parameter into `Debounce` and `DelayTextOnKeyPressInterval` into DebounceInterval.
- Replace all `Left` and `Right` enums with `Start` and `End` for the following enum types: `Direction`, `Float`, `Placement`, `NotificationLocation`, `Side`, `SnackbarLocation`, `SnackbarStackLocation`, `TabPosition`, and `TextAlignment`.
- Replace all `FromLeft`, `FromRight`, `RoundedLeft`, and `RoundedRight` enums with `FromStart`, `FromEnd`, `RoundedStart`, and `RoundedEnd` for the `Border` utilities.
- Replace all `FromLeft` and `FromRight` with `FromStart`, `FromEnd` for the Margin and `Padding` utilities.
- Replace all `AddLabel` with `AddLabels` method on chart instance.
- Change enum value from `None` to `Default` for the following enum types: `Color`, `Background`, `TextColor`, `Alignment`, `BorderRadius`, `BorderSize`, `Direction`, `DisplayDirection`, `FigureSize`, `IconSize`, `JustifyContent`, `OverflowType`, `SnackbarColor`, `Target`, `TextAlignment`, `TextOverflow`, `TextTransform`, `TextWeight`, `VerticalAlignment`, `Visibility`, `Size`, and `SnackbarLocation`.
- Obsolete typography parameters `Alignment`, `Color`, `Transform`, and `Weight` are removed in favor of `TextAlignment`, `TextColor`, `TextTransform`, and `TextWeight`.
- Remove any use of an obsolete component `<InlineField>`.
- The Datagrid's obsolete `Direction` parameter has now been removed. Instead, please use the `SortDirection` parameter if you weren't already..
- Rename `<Tabs>` `Mode` parameter into `RenderMode`.
> _Check out [Blazorise Release Notes](https://preview.blazorise.com/news/release-notes/100) for more information._
## MVC - Razor Pages UI
If you use MVC Razor Pages UI, you should follow this section.
### Client libraries
The `libs` folder no longer exists in templates after v5.2. That change greatly reduced the size of templates and brought some other advantages.
You can use `abp install-libs` command for installing or updating client libraries. You should run this command after updating v5.2.
> If you're creating a new project, you don't have to be concerned about it, ABP CLI installs client libraries after automatically.
## See Also
* [Official blog post for the 5.2 release](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published)

3
docs/en/Migration-Guides/Index.md

@ -1,5 +1,8 @@
# ABP Framework Migration Guides
The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../Upgrading.md).
- [5.1 to 5.2](Abp-5_2.md)
- [4.x to 5.0](Abp-5_0.md)
- [4.2 to 4.3](Abp-4_3.md)
- [4.x to 4.2](Abp-4_2.md)

43
docs/en/Module-Development-Basics.md

@ -29,6 +29,8 @@ public class BlogModule : AbpModule
``ConfigureServices`` is the main method to add your services to the dependency injection system and configure other modules. Example:
> These methods have Async versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones.
````C#
public class BlogModule : AbpModule
{
@ -57,26 +59,31 @@ public class BlogModule : AbpModule
}
````
> `ConfigureServices` method has an asynchronous version too: `ConfigureServicesAsync`. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one. If you override both asynchronous and synchronous versions, only the asynchronous version will be executed.
See the [Configuration](Configuration.md) document for more about the configuration system.
#### Pre & Post Configure Services
``AbpModule`` class also defines ``PreConfigureServices`` and ``PostConfigureServices`` methods to override and write your code just before and just after ``ConfigureServices``. Notice that the code you have written into these methods will be executed before/after the ``ConfigureServices`` methods of all other modules.
> These methods have asynchronous versions too. If you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones.
### Application Initialization
Once all the services of all modules are configured, the application starts by initializing all modules. In this phase, you can resolve services from ``IServiceProvider`` since it's ready and available.
#### OnApplicationInitialization Method
You can override ``OnApplicationInitialization`` method to execute code while application is being started. Example:
You can override ``OnApplicationInitialization`` method to execute code while application is being started.
**Example:**
````C#
public class BlogModule : AbpModule
{
//...
public override void OnApplicationInitialization(ApplicationInitializationContext context)
public override void OnApplicationInitialization(
ApplicationInitializationContext context)
{
var myService = context.ServiceProvider.GetService<MyService>();
myService.DoSomething();
@ -84,14 +91,32 @@ public class BlogModule : AbpModule
}
````
``OnApplicationInitialization`` is generally used by the startup module to construct the middleware pipeline for ASP.NET Core applications. Example:
`OnApplicationInitialization` method has an asynchronous version too. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one.
**Example:**
````csharp
public class BlogModule : AbpModule
{
public override Task OnApplicationInitializationAsync(
ApplicationInitializationContext context)
{
var myService = context.ServiceProvider.GetService<MyService>();
await myService.DoSomethingAsync();
}
}
````
> If you override both asynchronous and synchronous versions, only the asynchronous version will be executed.
``OnApplicationInitialization`` is generally used by the startup module to construct the middleware pipeline for ASP.NET Core applications.
**Example:**
````C#
[DependsOn(typeof(AbpAspNetCoreMvcModule))]
public class AppModule : AbpModule
{
//...
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
@ -113,10 +138,14 @@ You can also perform startup logic if your module requires it
``AbpModule`` class also defines ``OnPreApplicationInitialization`` and ``OnPostApplicationInitialization`` methods to override and write your code just before and just after ``OnApplicationInitialization``. Notice that the code you have written into these methods will be executed before/after the ``OnApplicationInitialization`` methods of all other modules.
> These methods have asynchronous versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones.
### Application Shutdown
Lastly, you can override ``OnApplicationShutdown`` method if you want to execute some code while application is being shutdown.
> This methods has asynchronous version too. If you want to make asynchronous calls inside this method, override the asynchronous version instead of the synchronous one.
## Module Dependencies
In a modular application, it's not unusual for one module to depend upon another module(s). An Abp module must declare ``[DependsOn]`` attribute if it does have a dependency upon another module, as shown below:

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

Loading…
Cancel
Save