Browse Source

Merge branch 'dev' into EngincanV/update-appnolayers-docs

pull/15092/head
Engincan VESKE 3 years ago
parent
commit
a656c54bf8
  1. 31
      .github/workflows/spellcheck.yml
  2. 4
      .gitignore
  3. 4
      Directory.Build.props
  4. 2
      README.md
  5. 40
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  6. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  7. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json
  8. 17
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  9. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json
  10. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json
  11. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json
  12. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  13. 151
      cSpell.json
  14. 49
      docs/en/Application-Startup.md
  15. 75
      docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md
  16. 68
      docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md
  17. BIN
      docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg
  18. BIN
      docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg
  19. 2
      docs/en/Customizing-Application-Modules-Extending-Entities.md
  20. 68
      docs/en/Distributed-Event-Bus.md
  21. 15
      docs/en/Entities.md
  22. 14
      docs/en/Getting-Started-React-Native.md
  23. 10
      docs/en/Getting-Started-Running-Solution.md
  24. 6
      docs/en/JSON.md
  25. 10
      docs/en/KB/Windows-Path-Too-Long-Fix.md
  26. 99
      docs/en/Migration-Guides/Abp-7_0.md
  27. 2
      docs/en/Migration-Guides/OpenIddict-Angular.md
  28. 2
      docs/en/Migration-Guides/OpenIddict-Blazor-Server.md
  29. 2
      docs/en/Migration-Guides/OpenIddict-Blazor.md
  30. 2
      docs/en/Migration-Guides/OpenIddict-Mvc.md
  31. 2
      docs/en/Migration-Guides/OpenIddict-Step-by-Step.md
  32. 4
      docs/en/Module-Entity-Extensions.md
  33. 2
      docs/en/Modules/Account.md
  34. 2
      docs/en/Modules/Audit-Logging.md
  35. 2
      docs/en/Modules/Background-Jobs.md
  36. 17
      docs/en/Modules/Cms-Kit/Comments.md
  37. 42
      docs/en/Modules/Docs.md
  38. 2
      docs/en/Modules/Feature-Management.md
  39. 2
      docs/en/Modules/Identity.md
  40. 2
      docs/en/Modules/IdentityServer.md
  41. 6
      docs/en/Modules/OpenIddict.md
  42. 2
      docs/en/Modules/Permission-Management.md
  43. 2
      docs/en/Road-Map.md
  44. 2
      docs/en/Startup-Templates/Application-Single-Layer.md
  45. 2
      docs/en/Startup-Templates/Application.md
  46. 2
      docs/en/Startup-Templates/Module.md
  47. 4
      docs/en/Themes/LeptonXLite/Angular.md
  48. 2
      docs/en/Themes/LeptonXLite/AspNetCore.md
  49. 265
      docs/en/Tutorials/Part-1.md
  50. 859
      docs/en/Tutorials/Part-10.md
  51. 50
      docs/en/Tutorials/Part-2.md
  52. 134
      docs/en/Tutorials/Part-3.md
  53. 149
      docs/en/Tutorials/Part-4.md
  54. 188
      docs/en/Tutorials/Part-5.md
  55. 202
      docs/en/Tutorials/Part-6.md
  56. 139
      docs/en/Tutorials/Part-7.md
  57. 387
      docs/en/Tutorials/Part-8.md
  58. 458
      docs/en/Tutorials/Part-9.md
  59. 36
      docs/en/Tutorials/Todo/Index.md
  60. BIN
      docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png
  61. BIN
      docs/en/Tutorials/Todo/todo-list.png
  62. BIN
      docs/en/Tutorials/Todo/todo-ui-initial.png
  63. BIN
      docs/en/Tutorials/images/blazor-add-book-button-2.png
  64. BIN
      docs/en/Tutorials/images/blazor-bookstore-book-list-2.png
  65. BIN
      docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png
  66. BIN
      docs/en/Tutorials/images/blazor-delete-book-action-2.png
  67. BIN
      docs/en/Tutorials/images/blazor-edit-book-action-3.png
  68. BIN
      docs/en/Tutorials/images/blazor-edit-book-modal-2.png
  69. BIN
      docs/en/Tutorials/images/blazor-new-book-modal-2.png
  70. BIN
      docs/en/Tutorials/images/book-create-modal-with-author-2.png
  71. BIN
      docs/en/Tutorials/images/bookstore-actions-buttons-2.png
  72. BIN
      docs/en/Tutorials/images/bookstore-added-author-to-book-list-2.png
  73. BIN
      docs/en/Tutorials/images/bookstore-added-authors-to-modals-2.png
  74. BIN
      docs/en/Tutorials/images/bookstore-angular-author-selection-2.png
  75. BIN
      docs/en/Tutorials/images/bookstore-angular-authors-page-2.png
  76. BIN
      docs/en/Tutorials/images/bookstore-author-permissions-2.png
  77. BIN
      docs/en/Tutorials/images/bookstore-author-permissions-3.png
  78. BIN
      docs/en/Tutorials/images/bookstore-authors-page-2.png
  79. BIN
      docs/en/Tutorials/images/bookstore-authors-page-3.png
  80. BIN
      docs/en/Tutorials/images/bookstore-book-list-4.png
  81. BIN
      docs/en/Tutorials/images/bookstore-book-list-angular.png
  82. BIN
      docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png
  83. BIN
      docs/en/Tutorials/images/bookstore-confirmation-popup-2.png
  84. BIN
      docs/en/Tutorials/images/bookstore-create-dialog-3.png
  85. BIN
      docs/en/Tutorials/images/bookstore-edit-button-3.png
  86. BIN
      docs/en/Tutorials/images/bookstore-edit-delete-actions-2.png
  87. BIN
      docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png
  88. BIN
      docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png
  89. BIN
      docs/en/Tutorials/images/bookstore-new-author-modal-2.png
  90. BIN
      docs/en/Tutorials/images/bookstore-new-book-button-3.png
  91. BIN
      docs/en/Tutorials/images/bookstore-new-book-button-small-2.png
  92. BIN
      docs/en/Tutorials/images/bookstore-new-book-form-v3.png
  93. BIN
      docs/en/Tutorials/images/bookstore-new-menu-item-2.png
  94. BIN
      docs/en/Tutorials/images/bookstore-permissions-ui-2.png
  95. 9
      docs/en/UI/Angular/Account-Module.md
  96. 21
      docs/en/UI/Angular/OAuth-Module.md
  97. 14
      docs/en/UI/Angular/Quick-Start.md
  98. 23
      docs/en/UI/Angular/Theme-Configurations.md
  99. 5
      docs/en/UI/AspNetCore/AutoComplete-Select.md
  100. 21
      docs/en/UI/AspNetCore/Forms-Validation.md

31
.github/workflows/spellcheck.yml

@ -1,31 +0,0 @@
name: Documentation Checks
on:
push:
branches:
- dev
paths:
# This ensures the check will only be run when something changes in the docs content
- "docs/en/**/*"
pull_request:
branches:
- dev
paths:
- "docs/en/**/*"
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
spellcheck:
name: "Docs: Spellcheck (En)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Check out the code
- uses: actions/setup-node@v1
name: Setup node
with:
node-version: "16"
- run: npm install -g cspell
name: Install cSpell
- run: cspell --config ./cSpell.json "docs/en/**/*.md" --no-progress # Update for path to the markdown files
name: Run cSpell

4
.gitignore

@ -326,4 +326,6 @@ deploy/_run_all_log.txt
# No commit yarn.lock files in the subfolders of templates directory
templates/**/yarn.lock
templates/**/yarn.lock
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt
templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json

4
Directory.Build.props

@ -5,7 +5,7 @@
<MicrosoftAspNetCorePackageVersion>7.0.0</MicrosoftAspNetCorePackageVersion>
<!-- All Microsoft EntityFrameworkCore packages -->
<MicrosoftEntityFrameworkCorePackageVersion>7.0.0</MicrosoftEntityFrameworkCorePackageVersion>
<MicrosoftEntityFrameworkCorePackageVersion>7.0.1</MicrosoftEntityFrameworkCorePackageVersion>
<!-- All Microsoft packages -->
<MicrosoftPackageVersion>7.0.0</MicrosoftPackageVersion>
@ -17,7 +17,7 @@
<NSubstitutePackageVersion>4.3.0</NSubstitutePackageVersion>
<!-- Shouldly https://www.nuget.org/packages/Shouldly -->
<ShouldlyPackageVersion>4.0.3</ShouldlyPackageVersion>
<ShouldlyPackageVersion>4.1.0</ShouldlyPackageVersion>
<!-- xunit https://www.nuget.org/packages/xUnit -->
<xUnitPackageVersion>2.4.1</xUnitPackageVersion>

2
README.md

@ -1,6 +1,6 @@
# ABP Framework
![build and test](https://img.shields.io/github/workflow/status/abpframework/abp/build%20and%20test/dev?style=flat-square)
![build and test](https://img.shields.io/github/actions/workflow/status/abpframework/abp/build-and-test.yml?branch=dev&style=flat-square)
[![codecov](https://codecov.io/gh/abpframework/abp/branch/dev/graph/badge.svg?token=jUKLCxa6HF)](https://codecov.io/gh/abpframework/abp)
[![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
[![NuGet (with prereleases)](https://img.shields.io/nuget/vpre/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)

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

@ -409,6 +409,44 @@
"OverallDiscountPrice": "Overall Discount Price",
"OverallDiscountText": "Overall Discount Text",
"SelectReport": "- Select Report -",
"NoDataAvailable": "No data available"
"NoDataAvailable": "No data available",
"StatisticsOfCachedContents": "Statistics of cached NuGet package contents for nuget.abp.io",
"Compact": "Compact",
"EditSettings": "Edit Settings",
"CurrentEstimatedSize": "Current Estimated Size",
"CurrentEntryCount": "Current Entry Count",
"TotalHits": "Total Hits",
"TotalMisses": "Total Misses",
"NoResponseFrom": "No response from",
"ContentCacheSlidingExpirationByDay": "Content Cache Sliding Expiration By Day",
"MaxDaysForCaching": "Max Days For Caching",
"Enabled": "Enabled",
"Menu:NugetPackagesContentCache": "NuGet Packages Content Cache",
"NugetPackagesContentCache": "NuGet Content Cache",
"SlidingExpritionByDayInfo": "Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. This will not extend the entry lifetime beyond the absolute expiration.",
"MaxDaysForCachingInfo": "Gets or sets an absolute expiration time, relative to now.",
"CurrentEstimatedSizeInfo": "Indicates an estimated sum of all the NuGet packages' content size currently in the memory cache",
"CurrentEntryCountInfo": "Indicates the number of instances currently in the memory cache.",
"TotalHitsInfo": "Indicates the total number of cache misses. A cache hit occurs when a file is requested from a cache and the cache is able to fulfill that request.",
"TotalMissesInfo": "Indicates the total number of cache hits. A cache miss is when the cache does not contain the requested content.",
"Permission:VersionHistory": "Version History",
"Caches": "Caches",
"VersionHistories": "Version History",
"Version": "Version",
"PublishDate": "Publish Date",
"IsStableVersion": "Stable Version",
"IsActive": "Active",
"NewVersion": "New Version",
"VersionHistoryDeletionConfirmationMessage": "Are you sure you want to delete this version?",
"CreateAbpConsultantLogoInfo": "Maximum file size: <b>1MB</b> <br/> Supported file types: <b>jpg, jpeg, png, SVG, WebP</b>",
"UrlCode": "Url Code",
"Icon": "Icon",
"Clear": "Clear",
"Permission:AbpConsultant": "ABP Consultant",
"Menu:AbpConsultants": "ABP Consultants",
"CreateAbpConsultant": "Create ABP Consultant",
"UrlCodeIsNotAvailable": "Url code is used by another ABP Consultant.",
"AbpConsultants": "ABP Consultants",
"AbpConsultant": "ABP Consultant"
}
}

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

@ -26,6 +26,7 @@
"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!",
"Volo.AbpIo.Domain:030012": "A user is entitled to have only 1 free trial period. You already used your trial 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!",
"WantToLearn?": "Want to learn?",
@ -183,6 +184,9 @@
"BlackFriday": "<strong>BLACK</strong> <span>FRIDAY</span>",
"ValidForExistingCustomers": "Also valid for the <br> existing customers!",
"CampaignBetweenDates": "From {0} <br>to {1}",
"SaveUpTo": "<span>SAVE</span> UP TO<strong>${0}K</strong>"
"SaveUpTo": "<span>SAVE</span> UP TO<strong>${0}K</strong>",
"ImplementingDDD": "Implementing Domain Driven Design",
"ExploreTheEBook": "Explore the E-Book",
"ExploreTheBook": "Explore the Book"
}
}

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

@ -711,7 +711,7 @@
"WatchTakeCloserLookVideo": "شاهد فيديو \"Take a closer look at the code generation: ABP Suite\"!",
"ConfirmedEmailAddressRequiredToStartTrial": "يجب أن يكون لديك عنوان بريد إلكتروني مؤكد لبدء ترخيص تجريبي.",
"EmailVerificationMailNotSent": "تعذر إرسال بريد التحقق من البريد الإلكتروني.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\">انقر هنا للحصول على بريد إلكتروني للتأكيد</a> إذا لم تكن قد حصلت عليه من قبل.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\" class=\"text-decoration-underline\">انقر هنا للحصول على بريد إلكتروني للتأكيد</a> إذا لم تكن قد حصلت عليه من قبل.",
"WhichLicenseTypeYouAreInterestedIn": "ما نوع الرخصة المهتم بها؟",
"DontTakeOurWordForIt": "لا تأخذ كلمتنا لذلك...",
"ReadAbpCommercialUsersWantYouToKnow": "اقرأ ما يريد مستخدمو ABP التجاري أن تعرفه",

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

@ -735,7 +735,7 @@
"WatchTakeCloserLookVideo": "Watch the \"Take a closer look at the code generation: ABP Suite\" Video!",
"ConfirmedEmailAddressRequiredToStartTrial": "You should have a confirmed email address in order to start a trial license.",
"EmailVerificationMailNotSent": "Email verification mail couldn't send.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\">Click here to get a verification email</a> if you haven't got it before.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\" class=\"text-decoration-underline\">Click here to get a verification email</a> if you haven't got it before.",
"WhichLicenseTypeYouAreInterestedIn": "Which license type you are interested in?",
"DontTakeOurWordForIt": "Don't take our word for it...",
"ReadAbpCommercialUsersWantYouToKnow": "Read what ABP Commercial users want you to know",
@ -806,6 +806,19 @@
"SupportPolicyFaqTitle": "What is your support policy?",
"SupportPolicyFaqExplanation": "We do support only the active and the previous major version. We do not guarantee a patch release for the 3rd and older major versions. For example, if the active version is 7.0.0, we will release patch releases for both 6.x.x and 7.x.x. Besides, we provide support only for ABP Framework and ABP Commercial related issues. That means no support is given for the 3rd party applications, cloud services and other peripheral libraries used by ABP products. We will use commercially reasonable efforts to provide our customers with technical support during \"Volosoft Bilisim A.S\"s official business hours. On the other hand, we do not commit to a service-level agreement (SLA) response time, but we will try to respond to the technical issues as quickly as possible within our official working hours. Unless a special agreement is made with the customer, we only provide support at https://support.abp.io. We also have private email support, which is only available to Enterprise License holders.",
"TotalDevelopers": "Total {0} developer(s)",
"CustomPurchaseExplanation": "Tailored to your specific needs"
"CustomPurchaseExplanation": "Tailored to your specific needs",
"WhereDidYouHearAboutUs": "Where did you hear about us?",
"Twitter": "Twitter",
"Facebook": "Facebook",
"Youtube": "YouTube",
"Google": "Google",
"Github": "GitHub",
"Friend": " From a friend",
"Other": "Other",
"WhereDidYouHearAboutUs_explain": "Specify ...",
"DeletingMemberWarningMessage": "\"{0}\" will be removed from the developer list. If you want, you can assign this empty seat to another developer later.",
"AdditionalInfo": "If the developer seats are above your requirements, you can reduce them. You can email at <a href=\"mailto:info@abp.io\">info@abp.io</a> to remove some of your developer seats. Clearing unused developer seats will reduce the license renewal cost. If you want, you can re-purchase additional developer seats within your active license period. Note that, since there are {0} developers in this license package, you cannot reduce this number.",
"LinkExpiredErrorMessage": "The link you are trying to access is expired.",
"ExpirationDate": "Expiration Date"
}
}

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

@ -735,7 +735,7 @@
"WatchTakeCloserLookVideo": "Tekintse meg a „Nézze meg közelebbről a kódgenerálást: ABP Suite” videót!",
"ConfirmedEmailAddressRequiredToStartTrial": "A próbalicenc elindításához rendelkeznie kell egy megerősített e-mail címmel.",
"EmailVerificationMailNotSent": "Nem sikerült elküldeni az ellenőrző e-mailt.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\">Kattintson ide, ha megerősítő e-mailt szeretne kapni,</a> ha még nem kapta meg.",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\" class=\"text-decoration-underline\">Kattintson ide, ha megerősítő e-mailt szeretne kapni,</a> ha még nem kapta meg.",
"WhichLicenseTypeYouAreInterestedIn": "Melyik licenctípus érdekli?",
"DontTakeOurWordForIt": "Ne fogadd el a szavunkat...",
"ReadAbpCommercialUsersWantYouToKnow": "Olvassa el, hogy az ABP Commercial felhasználói mit szeretnének tudni",

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

@ -744,7 +744,7 @@
"WatchTakeCloserLookVideo": "\"Kod üretimine daha yakından bakın: ABP Suite\" videosunu izleyin!",
"ConfirmedEmailAddressRequiredToStartTrial": "Deneme lisansı başlatmak için onaylanmış bir e -posta adresiniz olmalı.",
"EmailVerificationMailNotSent": "E-posta doğrulama postası gönderilemedi.",
"GetConfirmationEmail": "Daha önce bir onay e-postası almadıysanız <a href=\"javascript:void(0);\" id=\"{0}\"> almak için buraya tıklayın.</a>",
"GetConfirmationEmail": "Daha önce bir onay e-postası almadıysanız <a href=\"javascript:void(0);\" id=\"{0}\" class=\"text-decoration-underline\"> almak için buraya tıklayın.</a>",
"WhichLicenseTypeYouAreInterestedIn": "Hangi lisans türüyle ilgileniyorsunuz?",
"BlackFridayDiscount": "Kara Cuma İndirimi"
}

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

@ -735,7 +735,7 @@
"WatchTakeCloserLookVideo": "观看“详细了解ABP Suite 的代码生成”视频!",
"ConfirmedEmailAddressRequiredToStartTrial": "你应该有一个确认的电子邮件地址,以便开始试用许可证。",
"EmailVerificationMailNotSent": "电子邮件验证邮件不能发送。",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\">点击这里获取确认邮件</a> 如果你还没有收到。",
"GetConfirmationEmail": "<a href=\"javascript:void(0);\" id=\"{0}\" class=\"text-decoration-underline\">点击这里获取确认邮件</a> 如果你还没有收到。",
"WhichLicenseTypeYouAreInterestedIn": "你感兴趣的许可证类型是什么?",
"DontTakeOurWordForIt": "不要相信我们的话......",
"ReadAbpCommercialUsersWantYouToKnow": "阅读 ABP Commercial 用户希望您了解到的内容",

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

@ -409,9 +409,9 @@
"SeeTheScreenshot": "See the screenshot",
"ApplicationModuleExplanation1": "Creates a reusable, fully layered application module solution.",
"ApplicationModuleExplanation2": "You can use this option to create modules for your modular application.",
"Expert": "Expert",
"Expert": "ABP Expert",
"Expert_": "Expert",
"Partner": "Partner",
"Partner": "ABP Partner",
"Partner_": "Partnership",
"WebSite": "Web Site",
"Industry": "Industry",
@ -428,6 +428,7 @@
"Date": "Date",
"Activity": "Activity",
"Type": "Type",
"Contribution": "Contribution"
"Contribution": "Contribution",
"Info": "Info"
}
}

151
cSpell.json

@ -1,151 +0,0 @@
{
"version": "0.2",
"language": "en",
"words": [
"ABP's",
"abpframework",
"Antiforgery",
"appsettings",
"aspnet",
"aspnetcore",
"Autofac",
"automagically",
"Blazor",
"CQRS",
"crossfade",
"Dapr",
"Datagrid's",
"Datatable",
"datepicker",
"dismissable",
"dockerized",
"entrypoints",
"findability",
"hoverable",
"Iddict",
"IntelliCode",
"Keysize",
"Linq",
"Microservices",
"middlewares",
"Minifier",
"multitenancy",
"multitenant",
"Navs",
"Newtonsoft",
"Npgsql",
"oidc",
"overridable",
"Parameterless",
"Passwordless",
"PKCE",
"preconfigured",
"proxying",
"redirections",
"scrollbars",
"signin",
"Templating",
"textboxes",
"toolset",
"unsubscription",
"Xunit"
],
"ignoreWords": [
"Aliyun",
"Allibone",
"Blazorise",
"Boutwell",
"Cmskit",
"connectionstrings",
"Devart",
"Formik",
"Halil",
"Hanselman",
"hikalkan",
"Ibrahim",
"İbrahim",
"Kalkan",
"Kirti",
"Kommunity",
"Kulkarni",
"Luxon",
"malihu",
"Malik",
"Masis",
"Minio",
"NGXS",
"NSWAG",
"Scriban",
"Serilog",
"Shoudly",
"Shouldly",
"Sweetalert",
"Syncfusion",
"Telerik",
"Timeago",
"Toastr",
"Volo",
"Volosoft",
"Xeevis"
],
"patterns": [
{
"name": "Markdown links",
"pattern": "\\((.*)\\)",
"description": ""
},
{
"name": "Markdown code blocks",
"pattern": "/^(\\s*`{3,}).*[\\s\\S]*?^\\1/gmx",
"description": "Taken from the cSpell example at https://cspell.org/configuration/patterns/#verbose-regular-expressions"
},
{
"name": "Inline code blocks",
"pattern": "\\`([^\\`\\r\\n]+?)\\`",
"description": "https://stackoverflow.com/questions/41274241/how-to-capture-inline-markdown-code-but-not-a-markdown-code-fence-with-regex"
},
{
"name": "Link contents",
"pattern": "\\<a(.*)\\>",
"description": ""
},
{
"name": "Snippet references",
"pattern": "-- snippet:(.*)",
"description": ""
},
{
"name": "Snippet references 2",
"pattern": "\\<\\[sample:(.*)",
"description": "another kind of snippet reference"
},
{
"name": "Multi-line code blocks",
"pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm"
},
{
"name": "HTML Tags",
"pattern": "<[^>]*>",
"description": "Reference: https://stackoverflow.com/questions/11229831/regular-expression-to-remove-html-tags-from-a-string"
},
{
"name": "Markdown Image",
"pattern": "!\\[(.*)\\]\\((.*)\\)"
}
],
"ignoreRegExpList": [
"Markdown links",
"Markdown code blocks",
"Inline code blocks",
"Link contents",
"Snippet references",
"Snippet references 2",
"Multi-line code blocks",
"HTML Tags",
"Markdown Image"
],
"ignorePaths": [
"**/*Release/Post.md",
"**/*Preview/POST.md"
]
}

49
docs/en/Application-Startup.md

@ -204,6 +204,7 @@ We've passed a lambda method to configure the `ApplicationName` option. Here's a
* `ApplicationName`: A human-readable name for the application. It is a unique value for an application.
* `Configuration`: Can be used to setup the [application configuration](Configuration.md) when it is not provided by the hosting system. It is not needed for ASP.NET Core and other .NET hosted applications. However, if you've used `AbpApplicationFactory` with an internal service provider, you can use this option to configure how the application configuration is built.
* `Environment`: Environment name for the application.
* `PlugInSources`: A list of plugin sources. See the [Plug-In Modules documentation](PlugIn-Modules) to learn how to work with plugins.
* `Services`: The `IServiceCollection` object that can be used to register service dependencies. You generally don't need that, because you configure your services in your [module class](Module-Development-Basics.md). However, it can be used while writing extension methods for the `AbpApplicationCreationOptions` class.
@ -253,6 +254,54 @@ The `IAbpApplication` interface extends the `IApplicationInfoAccessor` interface
`IAbpApplication` is disposable. Always dispose of it before exiting your application.
## IAbpHostEnvironment
Sometimes, while creating an application, we need to get the current hosting environment and take actions according to that. In such cases, we can use some services such as [IWebHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-7.0) or [IWebAssemblyHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) provided by .NET, in the final application.
However, we can not use these services in a class library, which is used by the final application. ABP Framework provides the `IAbpHostEnvironment` service, which allows you to get the current environment name whenever you want. `IAbpHostEnvironment` is used by the ABP Framework in several places to perform specific actions by the environment. For example, ABP Framework reduces the cache duration on the **Development** environment for some services.
`IAbpHostEnvironment` obtains the current environment name by the following order:
1. Gets and sets the environment name if it's specified in the `AbpApplicationCreationOptions`.
2. Tries to obtain the environment name from the `IWebHostEnvironment` or `IWebAssemblyHostEnvironment` services for ASP.NET Core & Blazor WASM applications if the environment name isn't specified in the `AbpApplicationCreationOptions`.
3. Sets the environment name as **Production**, if the environment name is not specified or can not be obtained from the services.
You can configure the `AbpApplicationCreationOptions` [options class](Options.md) while creating the ABP application and set an environment name to its `Environment` property. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `Environment` option as shown below:
```csharp
await builder.AddApplicationAsync<OrderingServiceHttpApiHostModule>(options =>
{
options.Environment = Environments.Staging; //or directly set as "Staging"
});
```
Then, whenever you need to get the current environment name or check the environment, you can use the `IAbpHostEnvironment` interface:
```csharp
public class MyDemoService
{
private readonly IAbpHostEnvironment _abpHostEnvironment;
public MyDemoService(IAbpHostEnvironment abpHostEnvironment)
{
_abpHostEnvironment = abpHostEnvironment;
}
public void MyMethod()
{
var environmentName = _abpHostEnvironment.EnvironmentName;
if (_abpHostEnvironment.IsDevelopment()) { /* ... */ }
if (_abpHostEnvironment.IsStaging()) { /* ... */ }
if (_abpHostEnvironment.IsProduction()) { /* ... */ }
if (_abpHostEnvironment.IsEnvironment("custom-environment")) { /* ... */ }
}
}
```
## .NET Generic Host & ASP.NET Core Integrations
`AbpApplicationFactory` can create a standalone ABP application container without any external dependency. However, in most cases, you will want to integrate it with [.NET's generic host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host) or ASP.NET Core. For such usages, ABP provides built-in extension methods to easily create an ABP application container that is well-integrated to these systems.

75
docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md

@ -0,0 +1,75 @@
# ABP.IO Platform 7.0 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 7.0 versions have been released today.
## What's New With 7.0?
Since all the new features are already explained in detail in the [7.0 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-7.0-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-7.0-RC-Has-Been-Published) for all the features and enhancements.
## Getting Started with 7.0
### Creating New Solutions
You can create a new solution with the ABP Framework version 7.0 by either using the `abp new` command or generating the CLI command 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 it yet:
```bash
dotnet tool install -g Volo.Abp.Cli
```
To update an existing installation:
```bash
dotnet tool update -g Volo.Abp.Cli
```
#### Upgrading Existing Solutions with the 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 Guides
There are breaking changes in this version that may affect your application. Please see the following migration documents, if you are upgrading from v6.x:
* [ABP Framework 6.x to 7.0 Migration Guide](https://docs.abp.io/en/abp/7.0/Migration-Guides/Abp-7_0)
* [ABP Commercial 6.x to 7.0 Migration Guide](https://docs.abp.io/en/commercial/7.0/migration-guides/v7_0)
## Community News
### Highlights from .NET 7.0?
Our team has closely followed the ASP.NET Core and Entity Framework Core 7.0 releases, read Microsoft's guides and documentation and adapt the changes to our ABP.IO Platform. We are proud to say that we've shipped the ABP 7.0 RC.1 based on .NET 7.0 just after Microsoft's .NET 7.0 release.
In addition to the ABP's .NET 7.0 upgrade, our team has created 13 great articles to highlight the important features coming with ASP.NET Core 7.0 and Entity Framework Core 7.0.
You can read [this post](https://volosoft.com/Blog/Highlights-for-ASP.NET-Entity-Framework-Core-NET-7.0) to see the list of all articles.
### New ABP Community Posts
In addition to [the 13 articles to highlight .NET 7.0 features written by our team]((https://volosoft.com/Blog/Highlights-for-ASP.NET-Entity-Framework-Core-NET-7.0)), here are some of the recent posts added to the [ABP Community](https://community.abp.io/):
* [liangshiwei](https://github.com/realLiangshiwei) has created a new community article, that shows [How to Use the Weixin Authentication for MVC / Razor Page Applications](https://community.abp.io/posts/how-to-use-the-weixin-authentication-for-mvc-razor-page-applications-a33e0wti).
* [Jasen Fici](https://community.abp.io/posts/deploying-abp.io-to-an-azure-appservice-ma8kukdp) has created a new article: [Deploying abp.io to an Azure AppService](https://community.abp.io/posts/deploying-abp.io-to-an-azure-appservice-ma8kukdp).
Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://community.abp.io/articles/submit) to the ABP Community.
## About the Next Version
The next feature version will be 7.1. 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 problems with this version.

68
docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md

@ -0,0 +1,68 @@
# How to Use the Weixin Authentication for MVC / Razor Page Applications
This guide demonstrates how to integrate Weixin to an ABP application that enables users to sign in using OAuth 2.0 with credentials.
## Create a sandbox account
If you don't have a production account, you can create a sendbox account for testing: https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
In this article we will use the sandbox account.
> You should configure the callback domain name on the Weixin open platform
## AddWeixin
You need to install `AspNet.Security.OAuth.Weixin` package to your **.Web** project.
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following:
```csharp
private void ConfigureAuthentication(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
context.Services.AddAuthentication()
.AddWeixin(options =>
{
options.ClientId = configuration["Weixin:ClientId"];
options.ClientSecret = configuration["Weixin:ClientSecret"];
});
}
```
Updating `appsettings.json` to add `Weixin` section:
````json
"Weixin": {
"ClientId": "<your-app-id>",
"ClientSecret": "<your-app-secret>"
}
````
## Web page authorization
Now you can run the application to login with Weixin.
![login-with-weixin](login-with-weixin.jpg)
It will redirect to weixin platform to scan the QR code.
> The sandbox account lacks the necessary scope, so it may not work properly.
## Official account authorization
Updating `AddWeixin`:
```csharp
context.Services.AddAuthentication()
.AddWeixin(options =>
{
options.ClientId = configuration["Weixin:ClientId"];
options.ClientSecret = configuration["Weixin:ClientSecret"];
options.AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize";
});
```
Now you can use WeChat app to open the web application URL to login with weixin.
![offical-account](offical-account.jpg)

BIN
docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

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

@ -101,7 +101,7 @@ public class MyLocalIdentityUserChangeEventHandler :
````
* `EntityChangedEventData<T>` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes).
* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData<T>` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional.
* This code will be executed in the **current unit of work**, the whole process becomes transactional.
> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers).

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

@ -296,6 +296,74 @@ This example;
> 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.
## Entity Synchronizer
In a distributed (or microservice) system, it is typical to subscribe to change events for an [entity](Entities.md) type of another service, so you can get notifications when the subscribed entity changes. In that case, you can use ABP's Pre-Defined Events as explained in the previous section.
If your purpose is to store your local copies of a remote entity, you typically subscribe to create, update and delete events of the remote entity and update your local database in your event handler. ABP provides a pre-built `EntitySynchronizer` base class to make that operation easier for you.
Assume that there is a `Product` entity (probably an aggregate root entity) in a Catalog microservice, and you want to keep copies of the products in your Ordering microservice, with a local `OrderProduct` entity. In practice, properties of the `OrderProduct` class will be a subset of the `Product` properties, because not all the product data is needed in the Ordering microservice (however, you can make a full copy if you need). Also, the `OrderProduct` entity may have additional properties that are populated and used in the Ordering microservice.
The first step to establish the synchronization is to define an ETO (Event Transfer Object) class in the Catalog microservice that is used to transfer the event data. Assuming the `Product` entity has a `Guid` key, your ETO can be as shown below:
````
[EventName("product")]
public class ProductEto : EntityEto<Guid>
{
// Your Product properties here...
}
````
`ProductEto` can be put in a shared project (DLL) that is referenced by the Catalog and the Ordering microservices. Alternatively, you can put a copy of the `ProductEto` class in the Ordering microservice if you don't want to introduce a common project dependency between the services. In this case, the `EventName` attribute becomes critical to map the `ProductEto` classes across two services (you should use the same event name).
Once you define an ETO class, you should configure the ABP Framework to publish auto (create, update and delete) events for the `Product` entity, as explained in the previous section:
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>
{
options.AutoEventSelectors.Add<Product>();
options.EtoMappings.Add<Product, ProductEto>();
});
````
Finally, you should create a class in the Ordering microservice, that is derived from the `EntitySynchronizer` class:
````csharp
public class ProductSynchronizer : EntitySynchronizer<OrderProduct, Guid, ProductEto>
{
public ProductSynchronizer(
IObjectMapper objectMapper,
IRepository<OrderProduct, Guid> repository
) : base(objectMapper, repository)
{
}
}
````
The main point of this class is it subscribes to the create, update and delete events of the source entity and updates the local entity in the database. It uses the [Object Mapper](Object-To-Object-Mapping.md) system to create or update the `OrderProduct` objects from the `ProductEto` objects. So, you should also configure the object mapper to make it properly work. Otherwise, you should manually perform the object mapping by overriding the `MapToEntityAsync(TSourceEntityEto)` and `MapToEntityAsync(TSourceEntityEto,TEntity)` methods in your `ProductSynchronizer` class.
If your entity has a composite primary key (see the [Entities document](Entities.md)), then you should inherit from the `EntitySynchronizer<TEntity, TSourceEntityEto>` class (just don't use the `Guid` generic argument in the previous example) and implement `FindLocalEntityAsync` to find the entity in your local database using the `Repository`.
`EntitySynchronizer` is compatible with the *Entity Versioning* system (see the [Entities document](Entities.md)). So, it works as expected even if the events are received as disordered. If the entity's version in your local database is newer than the entity in the received event, then the event is ignored. You should implement the `IHasEntityVersion` interface for the entity and ETO classes (for this example, you should implement for the `Product`, `ProductEto` and `OrderProduct` classes).
If you want to ignore some type of change events, you can set `IgnoreEntityCreatedEvent`, `IgnoreEntityUpdatedEvent` and `IgnoreEntityDeletedEvent` in the constructor of your class. Example:
````csharp
public class ProductSynchronizer
: EntitySynchronizer<OrderProduct, Guid, ProductEto>
{
public ProductSynchronizer(
IObjectMapper objectMapper,
IRepository<OrderProduct, Guid> repository
) : base(objectMapper, repository)
{
IgnoreEntityDeletedEvent = true;
}
}
````
> Notice that the `EntitySynchronizer` can only create/update the entities after you use it. If you have an existing system with existing data, you should manually copy the data for one time, because the `EntitySynchronizer` starts to work.
## Transaction and Exception Handling
Distributed event bus works in-process (since default implementation is `LocalDistributedEventBus`) unless you configure an actual provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [RabbitMQ](Distributed-Event-Bus-RabbitMQ-Integration.md)). In-process event bus always executes event handlers in the same [unit of work](Unit-Of-Work.md) scope that you publishes the events in. That means, if an event handler throws an exception, then the related unit of work (the database transaction) is rolled back. In this way, your application logic and event handling logic becomes transactional (atomic) and consistent. If you want to ignore errors in an event handler, you must use a `try-catch` block in your handler and shouldn't re-throw the exception.

15
docs/en/Entities.md

@ -324,6 +324,21 @@ It's designed as read-only and automatically invalidates a cached entity if the
> See the [Entity Cache](Entity-Cache.md) documentation for more information.
## Versioning Entities
ABP defines the `IHasEntityVersion` interface for automatic versioning of your entities. It only provides a single `EntityVersion` property, as shown in the following code block:
````csharp
public interface IHasEntityVersion
{
int EntityVersion { get; }
}
````
If you implement the `IHasEntityVersion` interface, ABP automatically increases the `EntityVersion` value whenever you update your entity. The initial `EntityVersion` value will be `0`, when you first create an entity and save to the database.
> ABP can not increase the version if you directly execute SQL `UPDATE` commands in the database. It is your responsibility to increase the `EntityVersion` value in that case. Also, if you are using the aggregate pattern and change sub-collections of an aggregate root, it is your responsibility if you want to increase the version of the aggregate root object.
## Extra Properties
ABP defines the `IHasExtraProperties` interface that can be implemented by an entity to be able to dynamically set and get properties for the entity. `AggregateRoot` base class already implements the `IHasExtraProperties` interface. If you've derived from this class (or one of the related audit class defined above), you can directly use the API.

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

@ -95,6 +95,20 @@ A React Native application running on an Android emulator or a physical phone **
Run the backend application as described in the [getting started document](Getting-Started.md).
> You should turn off the "Https Restriction" if you're using OpenIddict as a central identity management solution. Because the IOS Simulator doesn't support self-signed certificates and OpenIddict is set to only work with HTTPS by default.
## How to disable the Https-only settings of OpenIddict
Go to MyProjectNameHttpApiHostModule.cs under the host project. Add put these codes under the `PreConfigureServices` function.
```csharp
#if DEBUG
PreConfigure<OpenIddictServerBuilder>(options => {
options.UseAspNetCore()
.DisableTransportSecurityRequirement();
});
#endif
```
## How to Configure & Run the React Native Application

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

@ -113,11 +113,11 @@ This is the HTTP API that is used by the web application.
3. Lastly, ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the startup project and run the application which will open a **welcome** page in your browser
![mvc-tiered-app-home](images/bookstore-home.png)
![mvc-tiered-app-home](images/bookstore-home-2.png)
Click to the **login** button which will redirect you to the *authentication server* to login to the application:
![bookstore-login](images/bookstore-login.png)
![bookstore-login](images/bookstore-login-2.png)
{{ else # Tiered != "Yes" }}
@ -125,7 +125,7 @@ Ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the st
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
![bookstore-login](images/bookstore-login.png)
![bookstore-login](images/bookstore-login-2.png)
{{ end # Tiered }}
@ -171,7 +171,7 @@ Ensure that the `.Blazor` project is the startup project and run the application
Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password:
![bookstore-login](images/bookstore-login.png)
![bookstore-login](images/bookstore-login-2.png)
{{ else if UI == "NG" }}
@ -191,7 +191,7 @@ yarn start
It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address.
![bookstore-login](images/bookstore-login.png)
![bookstore-login](images/bookstore-login-2.png)
{{ end }}

6
docs/en/JSON.md

@ -66,3 +66,9 @@ Add [Volo.Abp.Json.Newtonsoft](https://www.nuget.org/packages/Volo.Abp.Json.Newt
#### AbpNewtonsoftJsonSerializerOptions
- **JsonSerializerSettings(`Newtonsoft.Json.JsonSerializerSettings`)**: Global options for Newtonsoft library operations. See [here](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonSerializerSettings.htm) for reference.
## Configuring JSON options in ASP.NET Core
You can change the JSON behavior in ASP.NET Core by configuring [JsonOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions) or
[MvcNewtonsoftJsonOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions)(if you use `Newtonsoft.Json`)

10
docs/en/KB/Windows-Path-Too-Long-Fix.md

@ -0,0 +1,10 @@
# How to Fix "Filename too long" Error on Windows
If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
If you face long path errors related to Git, try the following command to enable long paths in Windows.
```
git config --system core.longpaths true
```
See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path

99
docs/en/Migration-Guides/Abp-7_0.md

@ -110,9 +110,104 @@ See https://github.com/abpframework/abp/pull/13845 for more info.
> You can ignore this if you don't use CMS Kit Module.
## Oracle.EntityFrameworkCore and Devart.Data.Oracle.EFCore
## Data migration environment
Please call `AddDataMigrationEnvironment` method in the migration project.
```cs
using (var application = await AbpApplicationFactory.CreateAsync<MyMigratorModule>(options =>
{
//...
options.AddDataMigrationEnvironment();
}))
{
//...
}
```
```cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataMigrationEnvironment();
// Call AddDataMigrationEnvironment before AddApplicationAsync
await builder.AddApplicationAsync<MyMigratorModule>();
//...
```
These two packages do not yet support EF Core 7.0, If you use `AbpEntityFrameworkCoreOracleModule(Volo.Abp.EntityFrameworkCore.Oracle)` or `AbpEntityFrameworkCoreOracleDevartModule(Volo.Abp.EntityFrameworkCore.Oracle.Devart)` may not work as expected, We will release new packages as soon as they are updated.
See https://github.com/abpframework/abp/pull/13985 for more info.
## Devart.Data.Oracle.EFCore
The `Devart.Data.Oracle.EFCore` package do not yet support EF Core 7.0, If you use `AbpEntityFrameworkCoreOracleDevartModule(Volo.Abp.EntityFrameworkCore.Oracle.Devart)` may not work as expected, We will release new packages as soon as they are updated.
See https://github.com/abpframework/abp/issues/14412 for more info.
# Changes on Angular Apps
## Added a new package `@abp/ng.oauth`
OAuth Functionality moved to a seperate package named `@abp/ng.oauth`, so ABP users should add the `@abp/ng.oauth` packages on app.module.ts.
Add the new npm package to your app.
```
yarn add @abp/ng.oauth
// or npm i ---save @abp/ng.oauth
```
```typescript
// app.module.ts
import { AbpOAuthModule } from "@abp/ng.oauth";
// ...
@NgModule({
// ...
imports: [
AbpOAuthModule.forRoot(), // <-- Add This
// ...
],
// ...
})
export class AppModule {}
```
## Lepton X Google-Font
If you are using LeptonX that has google fonts, the fonts were built-in the Lepton file. It's been moved to a seperate file. So the ABP user should add font-bundle in angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' )
// for LeptonX Lite
```json
{
input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.rtl.css',
inject: false,
bundleName: 'font-bundle.rtl',
},
{
input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.css',
inject: false,
bundleName: 'font-bundle',
},
```
// for LeptonX
```json
{
input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.css',
inject: false,
bundleName: 'font-bundle',
},
{
input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.rtl.css',
inject: false,
bundleName: 'font-bundle.rtl',
},
```
## Updated Side Menu Layout
In side menu layout, eThemeLeptonXComponents.Navbar has been changed to eThemeLeptonXComponents.Toolbar, and
eThemeLeptonXComponents.Sidebar to eThemeLeptonXComponents.Navbar.
And also added new replaceable component like Logo Component, Language Component etc.
If you are using replaceable component system you can check [documentation](https://docs.abp.io/en/commercial/latest/themes/lepton-x/angular#customization).
## ng-zorro-antd-tree.css
ng-zorro-antd-tree.css file should be in angular.json if the user uses AbpTree component or Abp-commercial. The ABP User should add this style definition on angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' )
{ "input": "node_modules/ng-zorro-antd/tree/style/index.min.css", "inject": false, "bundleName": "ng-zorro-antd-tree" },

2
docs/en/Migration-Guides/OpenIddict-Angular.md

@ -112,7 +112,7 @@
## IdentityServer
This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
- In **MyApplication.IdentityServer.csproj** replace **project references**:

2
docs/en/Migration-Guides/OpenIddict-Blazor-Server.md

@ -103,7 +103,7 @@
## IdentityServer
This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
- In **MyApplication.IdentityServer.csproj** replace **project references**:

2
docs/en/Migration-Guides/OpenIddict-Blazor.md

@ -131,7 +131,7 @@
## IdentityServer
This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
- In **MyApplication.IdentityServer.csproj** replace **project references**:

2
docs/en/Migration-Guides/OpenIddict-Mvc.md

@ -111,7 +111,7 @@ Replace role scope to **roles** and add **UsePkce** and **SignoutScheme** option
## IdentityServer
This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future.
- In **MyApplication.IdentityServer.csproj** replace **project references**:

2
docs/en/Migration-Guides/OpenIddict-Step-by-Step.md

@ -1,6 +1,6 @@
# Migrating from IdentityServer to OpenIddict Step by Step Guide
This guide provides layer-by-layer guidance for migrating your existing application to [OpenIddict](https://github.com/openiddict/openiddict-core) from IdentityServer. ABP startup templates use `OpenIddict` OpenId provider from v6.0.0-rc1 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is only available with ABP v6.0, you will need to update your existing application in order to apply OpenIddict changes.
This guide provides layer-by-layer guidance for migrating your existing application to [OpenIddict](https://github.com/openiddict/openiddict-core) from IdentityServer. ABP startup templates use `OpenIddict` OpenId provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is only available with ABP v6.0, you will need to update your existing application in order to apply OpenIddict changes.
## History
We are not removing Identity Server packages and we will continue to release new versions of IdentityServer-related NuGet/NPM packages. That means you won't have an issue while upgrading to v6.0 when the stable version releases. We will continue to fix bugs in our packages for a while. ABP 7.0 will be based on .NET 7. If Identity Server continues to work with .NET 7, we will also continue to ship NuGet packages for our IDS integration.

4
docs/en/Module-Entity-Extensions.md

@ -312,12 +312,14 @@ An enum properties is shown as combobox (select) in the create/edit forms:
Enum member name is shown on the table and forms by default. If you want to localize it, just create a new entry on your [localization](https://docs.abp.io/en/abp/latest/Localization) file:
````json
"UserType.SuperUser": "Super user"
"Enum:UserType.0": "Super user"
````
One of the following names can be used as the localization key:
* `Enum:UserType.0`
* `Enum:UserType.SuperUser`
* `UserType.0`
* `UserType.SuperUser`
* `SuperUser`

2
docs/en/Modules/Account.md

@ -6,7 +6,7 @@ This module is based on [Microsoft's Identity library](https://docs.microsoft.co
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

2
docs/en/Modules/Audit-Logging.md

@ -6,7 +6,7 @@ The Audit Logging Module basically implements the `IAuditingStore` to save the a
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

2
docs/en/Modules/Background-Jobs.md

@ -6,7 +6,7 @@ The Background Jobs module implements the `IBackgroundJobStore` interface and ma
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

17
docs/en/Modules/Cms-Kit/Comments.md

@ -1,10 +1,10 @@
# CMS Kit: Comments
CMS kit provides a **comment** system to add comments feature to any kind of resource, like blog posts, products, etc.
CMS kit provides a **comment** system to add the comment feature to any kind of resource, like blog posts, products, etc.
## Options
The comment system provides a mechanism to group comment definitions by entity types. For example, if you want to use comment system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product`, and add comments under these entity types.
The comment system provides a mechanism to group comment definitions by entity types. For example, if you want to use the comment system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product`, and add comments under these entity types.
`CmsKitCommentOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example:
@ -12,6 +12,7 @@ The comment system provides a mechanism to group comment definitions by entity t
Configure<CmsKitCommentOptions>(options =>
{
options.EntityTypes.Add(new CommentEntityTypeDefinition("Product"));
options.IsRecaptchaEnabled = true; //false by default
});
```
@ -20,6 +21,7 @@ Configure<CmsKitCommentOptions>(options =>
`CmsKitCommentOptions` properties:
- `EntityTypes`: List of defined entity types(`CmsKitCommentOptions`) in the comment system.
- `IsRecaptchaEnabled`: This flag enables or disables the reCaptcha for the comment system. You can set it as **true** if you want to use reCaptcha in your comment system.
`CommentEntityTypeDefinition` properties:
@ -33,11 +35,12 @@ The comment system provides a commenting [widget](../../UI/AspNetCore/Widgets.md
@await Component.InvokeAsync(typeof(CommentingViewComponent), new
{
entityType = "Product",
entityId = "..."
entityId = "...",
referralLinks = new [] {"nofollow"}
})
```
`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here.
`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. `referralLinks` is an optional parameter. You can use this parameter to add values (such as "nofollow", "noreferrer", or any other values) to the [rel attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) of links.
## User Interface
@ -77,7 +80,7 @@ A comment represents a written comment from a user.
This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide.
Following custom repositories are defined for this feature:
The following custom repositories are defined for this feature:
- `ICommentRepository`
@ -93,8 +96,8 @@ This module follows the [Domain Services Best Practices & Conventions](https://d
#### Application services
- `CommentAdminAppService` (implements `ICommentAdminAppService`): Implements the use cases of comment management system, like listing or removing comments etc.
- `CommentPublicAppService` (implements `ICommentPublicAppService`): Implements the use cases of comment management on the public websites, like listing comments, adding comments etc.
- `CommentAdminAppService` (implements `ICommentAdminAppService`): Implements the use cases of the comment management system, like listing or removing comments etc.
- `CommentPublicAppService` (implements `ICommentPublicAppService`): Implements the use cases of the comment management on the public websites, like listing comments, adding comments etc.
### Database providers

42
docs/en/Modules/Docs.md

@ -28,9 +28,11 @@ If you do not have an existing ABP project, this step shows you how to create a
It is recommended to use ABP CLI to create new projects. Use the following command:
`abp new Acme.MyProject`
```bash
abp new Acme.MyProject
```
You can also navigate to https://abp.io/get-started. Enter your project name as `Acme.MyProject`, other use default options.
You can also generate a CLI command from [get started page](https://abp.io/get-started). Enter your project name as `Acme.MyProject`, other use default options.
Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider.
@ -66,7 +68,9 @@ Docs module packages are hosted on NuGet. There are 4 packages that needs be to
It is recommended to use the ABP CLI to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command:
`abp add-module Volo.Docs`
```bash
abp add-module Volo.Docs
```
#### 3.2- Manually install
@ -74,19 +78,27 @@ Or you can also manually install nuget package to each project:
* Install [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Domain` project.
`Install-Package Volo.Docs.Domain`
```bash
Install-Package Volo.Docs.Domain
```
* Install [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget package to `Acme.MyProject.EntityFrameworkCore` project.
`Install-Package Volo.Docs.EntityFrameworkCore`
```bash
Install-Package Volo.Docs.EntityFrameworkCore
```
* Install [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget package to `Acme.MyProject.Application` project.
`Install-Package Volo.Docs.Application`
```bash
Install-Package Volo.Docs.Application
```
* Install [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Web` project.
`Install-Package Volo.Docs.Web`
```bash
Install-Package Volo.Docs.Web
```
##### 3.2.1- Adding Module Dependencies
@ -258,7 +270,7 @@ If you choose Entity Framework as your database provider, you need to configure
The default route for Docs module is;
```
```txt
/Documents
```
@ -307,7 +319,7 @@ The new menu item for Docs Module is added to the menu. Run your web application
You will see a warning says;
```
```txt
There are no projects yet!
```
@ -487,14 +499,14 @@ For example [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/bl
```
.....
​````json
​```json
//[doc-params]
{
"UI": ["MVC","NG"],
"DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"]
}
​````
​```
........
```
@ -530,9 +542,9 @@ For example:
You can also use variables in a text, adding **_Value** postfix to its key:
````
```txt
This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider.
````
```
Also, **Document_Language_Code** and **Document_Version** keys are pre-defined if you want to get the language code or the version of the current document (This may be useful for creating links that redirects to another documentation system in another domain).
@ -603,7 +615,7 @@ Finally a new Docs Module is added to your project which is feeded with GitHub.
The Docs module supports full-text search using Elastic Search. It is not enabled by default. You can configure `DocsElasticSearchOptions` to enable it.
```
```csharp
Configure<DocsElasticSearchOptions>(options =>
{
options.Enable = true;
@ -616,7 +628,7 @@ The `Index` is automatically created after the application starts if the `Index`
`DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`.
If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](../Dependency-Injection.md) system.
```
```json
{
"ElasticSearch": {
"Url": "http://localhost:9200"

2
docs/en/Modules/Feature-Management.md

@ -6,7 +6,7 @@ The Feature Management module implements the `IFeatureManagementStore` interface
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

2
docs/en/Modules/Identity.md

@ -4,7 +4,7 @@ Identity module is used to manage roles, users and their permissions, based on t
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

2
docs/en/Modules/IdentityServer.md

@ -4,7 +4,7 @@ IdentityServer module provides a full integration with the [IdentityServer](http
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

6
docs/en/Modules/OpenIddict.md

@ -4,7 +4,7 @@ OpenIddict module provides an integration with the [OpenIddict](https://github.c
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as a package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as a package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code
@ -299,12 +299,12 @@ PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
The background task that automatically removes orphaned tokens/authorizations. This can be configured by `TokenCleanupOptions` to manage it.
`TokenCleanupOptions` can be configured in the `PreConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics).
`TokenCleanupOptions` can be configured in the `ConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics).
Example:
```csharp
PreConfigure<TokenCleanupOptions>(options =>
Configure<TokenCleanupOptions>(options =>
{
//Set options here...
});

2
docs/en/Modules/Permission-Management.md

@ -6,7 +6,7 @@ This module implements the `IPermissionStore` to store and manage permissions va
## How to Install
This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module.
### The Source Code

2
docs/en/Road-Map.md

@ -6,7 +6,7 @@ This document provides a road map, release schedule and planned features for the
### v7.1
The next version will be 7.1 and planned to release the stable 7.1 version in February, 2024. In the version 7.1, we will mostly focus on stabilizing and enhancing existing features, improving the developer experience, as well as adding relatively minor new features.
The next version will be 7.1 and planned to release the stable 7.1 version in February, 2023. In the version 7.1, we will mostly focus on stabilizing and enhancing existing features, improving the developer experience, as well as adding relatively minor new features.
See the [7.1 milestone](https://github.com/abpframework/abp/milestone/68) for all the issues we've planned to work on.

2
docs/en/Startup-Templates/Application-Single-Layer.md

@ -10,7 +10,7 @@ ABP's [Application Startup Template](Application.md) provides a well-organized a
## How to Start with It?
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download this startup template from the [Get Started](https://abp.io/get-started) page. In this section, we will use the ABP CLI.
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command for this startup template from the [Get Started](https://abp.io/get-started) page. In this section, we will use the ABP CLI.
Firstly, install the ABP CLI if you haven't installed it before:

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

@ -11,7 +11,7 @@ This document explains **the solution structure** and projects in details. If yo
## How to Start With?
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here.
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here.
First, install the ABP CLI if you haven't installed it before:

2
docs/en/Startup-Templates/Module.md

@ -4,7 +4,7 @@ This template can be used to create a **reusable [application module](../Modules
## How to Start With?
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here.
You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here.
First, install the ABP CLI if you haven't installed before:

4
docs/en/Themes/LeptonXLite/Angular.md

@ -20,7 +20,9 @@ To add `LeptonX-lite` into your project,
`yarn add bootstrap-icons`
- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one.
- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one in the following link :
* [Styles - Angular UI](../../UI/Angular/Theme-Configurations.md)
Add the following style

2
docs/en/Themes/LeptonXLite/AspNetCore.md

@ -38,7 +38,7 @@ Configure<AbpBundlingOptions>(options =>
// Remove the following line
- BasicThemeBundles.Styles.Global,
// Add the following line instead
+ LeptonXLiteThemeBundles.Styles.Global
+ LeptonXLiteThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-styles.css");

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

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
{{if UI == "MVC" && DB == "EF"}}
@ -66,18 +63,17 @@ The main entity of the application is the `Book`. Create a `Books` folder (names
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public class Book : AuditedAggregateRoot<Guid>
{
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public string Name { get; set; }
public BookType Type { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
public float Price { get; set; }
}
````
@ -92,20 +88,19 @@ namespace Acme.BookStore.Books
The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it:
````csharp
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public enum BookType
{
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
````
@ -153,34 +148,33 @@ Navigate to the `OnModelCreating` method in the `BookStoreDbContext` class and a
using Acme.BookStore.Books;
...
namespace Acme.BookStore.EntityFrameworkCore
namespace Acme.BookStore.EntityFrameworkCore;
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
...
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */
/* 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);
});
}
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);
});
}
}
````
@ -202,7 +196,7 @@ This will add a new migration class to the project:
![bookstore-efcore-migration](./images/bookstore-efcore-migration.png)
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{end}}
@ -220,44 +214,43 @@ using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IRepository<Book, Guid> _bookRepository;
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task SeedAsync(DataSeedContext context)
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
}
}
@ -290,18 +283,17 @@ In this section, you will create an application service to get, create, update a
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public class BookDto : AuditedEntityDto<Guid>
{
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
public string Name { get; set; }
public BookType Type { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
public float Price { get; set; }
}
````
@ -315,14 +307,13 @@ It will be needed to map the `Book` entities to the `BookDto` objects while retu
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore
namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{
public class BookStoreApplicationAutoMapperProfile : Profile
public BookStoreApplicationAutoMapperProfile()
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
}
CreateMap<Book, BookDto>();
}
}
````
@ -337,24 +328,23 @@ Create a `CreateUpdateBookDto` class in the `Books` folder (namespace) of the `A
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public class CreateUpdateBookDto
{
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
[Required]
public float Price { get; set; }
}
````
@ -367,15 +357,14 @@ As done to the `BookDto` above, we should define the mapping from the `CreateUpd
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore
namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{
public class BookStoreApplicationAutoMapperProfile : Profile
public BookStoreApplicationAutoMapperProfile()
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
````
@ -389,17 +378,16 @@ using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{
}
}
````
@ -417,22 +405,21 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
}
}
````

859
docs/en/Tutorials/Part-10.md

File diff suppressed because it is too large

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

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
{{if UI == "MVC" && DB == "EF"}}
@ -135,22 +132,22 @@ Open the `en.json` (*the English translations*) file and change the content as s
"CreationTime": "Creation time",
"AreYouSure": "Are you sure?",
"AreYouSureToDelete": "Are you sure you want to delete this item?",
"Enum:BookType.Undefined": "Undefined",
"Enum:BookType.Adventure": "Adventure",
"Enum:BookType.Biography": "Biography",
"Enum:BookType.Dystopia": "Dystopia",
"Enum:BookType.Fantastic": "Fantastic",
"Enum:BookType.Horror": "Horror",
"Enum:BookType.Science": "Science",
"Enum:BookType.ScienceFiction": "Science fiction",
"Enum:BookType.Poetry": "Poetry"
"Enum:BookType.0": "Undefined",
"Enum:BookType.1": "Adventure",
"Enum:BookType.2": "Biography",
"Enum:BookType.3": "Dystopia",
"Enum:BookType.4": "Fantastic",
"Enum:BookType.5": "Horror",
"Enum:BookType.6": "Science",
"Enum:BookType.7": "Science fiction",
"Enum:BookType.8": "Poetry"
}
}
````
* Localization key names are arbitrary. You can set any name. We prefer some conventions for specific text types;
* Add `Menu:` prefix for menu items.
* Use `Enum:<enum-type>.<enum-name>` or `<enum-type>.<enum-name>` or `<enum-name>` naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums in some proper cases.
* Use `Enum:<enum-type>.<enum-value>` or `<enum-type>.<enum-value>` naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums in some proper cases.
If a text is not defined in the localization file, it **falls back** to the localization key (as ASP.NET Core's standard behavior).
@ -181,14 +178,13 @@ Open the `Index.cshtml` and change the whole content as shown below:
```csharp
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Books
namespace Acme.BookStore.Web.Pages.Books;
public class IndexModel : PageModel
{
public class IndexModel : PageModel
public void OnGet()
{
public void OnGet()
{
}
}
}
```
@ -215,7 +211,7 @@ context.Menu.AddItem(
Run the project, login to the application with the username `admin` and the password `1q2w3E*` and you can see that the new menu item has been added to the main menu:
![bookstore-menu-items](images/bookstore-new-menu-item.png)
![bookstore-menu-items](images/bookstore-new-menu-item-2.png)
When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page.
@ -282,7 +278,7 @@ $(function () {
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
return l('Enum:BookType.' + data);
}
},
{
@ -328,7 +324,7 @@ $(function () {
You can run the application! The final UI of this part is shown below:
![Book list](images/bookstore-book-list-3.png)
![Book list](images/bookstore-book-list-4.png)
This is a fully working, server side paged, sorted and localized table of books.
@ -520,7 +516,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be
<ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
<ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
<ng-template let-row="row" ngx-datatable-cell-template>
{%{{{ '::Enum:BookType:' + row.type | abpLocalization }}}%}
{%{{{ '::Enum:BookType.' + row.type | abpLocalization }}}%}
</ng-template>
</ngx-datatable-column>
<ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
@ -540,7 +536,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be
Now you can see the final result on your browser:
![Book list final result](images/bookstore-book-list.png)
![Book list final result](images/bookstore-book-list-angular.png)
{{else if UI == "Blazor" || UI == "BlazorServer"}}
@ -584,7 +580,7 @@ context.Menu.AddItem(
Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu:
![blazor-menu-bookstore](images/blazor-menu-bookstore.png)
![blazor-menu-bookstore](images/bookstore-new-menu-item-2.png)
When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page.
@ -624,7 +620,7 @@ Open the `Books.razor` and replace the content as the following:
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType.{Enum.GetName(context.Type)}"]
@L[$"Enum:BookType.{context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
@ -667,7 +663,7 @@ We will continue benefitting from `AbpCrudPageBase` for the books page. You cou
You can run the application! The final UI of this part is shown below:
![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png)
![blazor-bookstore-book-list](images/blazor-bookstore-book-list-2.png)
This is a fully working, server side paged, sorted and localized table of books.

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

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
{{if UI == "MVC" && DB == "EF"}}
@ -53,13 +50,13 @@ This part is also recorded as a video tutorial and **<a href="https://www.youtub
In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like the image below:
![bookstore-create-dialog](images/bookstore-create-dialog-2.png)
![bookstore-create-dialog](./images/bookstore-create-dialog-3.png)
### Create the Modal Form
Create a new razor page named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project.
![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png)
![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png)
#### CreateModal.cshtml.cs
@ -191,7 +188,7 @@ The final content of `Index.cshtml` is shown below:
This adds a new button called **New book** to the **top-right** of the table:
![bookstore-new-book-button](images/bookstore-new-book-button-2.png)
![bookstore-new-book-button](./images/bookstore-new-book-button-3.png)
Open the `Pages/Books/Index.js` file and add the following code right after the `Datatable` configuration:
@ -235,7 +232,7 @@ $(function () {
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
return l('Enum:BookType.' + data);
}
},
{
@ -274,7 +271,7 @@ Now, you can **run the application** and add some new books using the new modal
Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project:
![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png)
![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png)
### EditModal.cshtml.cs
@ -286,35 +283,34 @@ using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books
namespace Acme.BookStore.Web.Pages.Books;
public class EditModalModel : BookStorePageModel
{
public class EditModalModel : BookStorePageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[BindProperty]
public CreateUpdateBookDto Book { get; set; }
[BindProperty]
public CreateUpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
}
public async Task OnGetAsync()
{
var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.UpdateAsync(Id, Book);
return NoContent();
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.UpdateAsync(Id, Book);
return NoContent();
}
}
````
@ -330,14 +326,13 @@ To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mappin
````csharp
using AutoMapper;
namespace Acme.BookStore.Web
namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{
public class BookStoreWebAutoMapperProfile : Profile
public BookStoreWebAutoMapperProfile()
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
CreateMap<BookDto, CreateUpdateBookDto>();
}
}
````
@ -421,7 +416,7 @@ $(function () {
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
return l('Enum:BookType.' + data);
}
},
{
@ -465,7 +460,9 @@ You can run the application and edit any book by selecting the edit action on a
The final UI looks as below:
![bookstore-books-table-actions](images/bookstore-edit-button-2.png)
![bookstore-books-table-actions](./images/bookstore-edit-button-3.png)
> Notice that you don't see the "Actions" button in the figure below. Instead, you see an "Edit" button. ABP is smart enough to show a single simple button instead of a actions dropdown button when the dropdown has only a single item. After the next section, it will turn to a drop down button.
## Deleting a Book
@ -557,7 +554,7 @@ $(function () {
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
return l('Enum:BookType.' + data);
}
},
{
@ -656,7 +653,7 @@ Open `/src/app/book/book.component.html` and make the following changes:
<!-- Add the "new book" button here -->
<div class="text-lg-end pt-2">
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<i class="fa fa-plus me-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button>
</div>
@ -690,7 +687,7 @@ Open `/src/app/book/book.component.html` and make the following changes:
You can open your browser and click the **New book** button to see the new modal.
![Empty modal for new book](images/bookstore-empty-new-book-modal.png)
![Empty modal for new book](./images/bookstore-empty-new-book-modal-2.png)
### Create a Reactive Form
@ -776,25 +773,25 @@ Open `/src/app/book/book.component.html` and replace `<ng-template #abpBody> </n
```html
<ng-template #abpBody>
<form [formGroup]="form" (ngSubmit)="save()">
<div class="form-group">
<div class="mt-2">
<label for="book-name">Name</label><span> * </span>
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus />
</div>
<div class="form-group">
<div class="mt-2">
<label for="book-price">Price</label><span> * </span>
<input type="number" id="book-price" class="form-control" formControlName="price" />
</div>
<div class="form-group">
<div class="mt-2">
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="type.value" *ngFor="let type of bookTypes"> {%{{{ type.key }}}%}</option>
<option [ngValue]="type.value" *ngFor="let type of bookTypes"> {%{{{ '::Enum:BookType.' + type.value | abpLocalization }}}%}</option>
</select>
</div>
<div class="form-group">
<div class="mt-2">
<label>Publish date</label><span> * </span>
<input
#datepicker="ngbDatepicker"
@ -927,7 +924,7 @@ export class BookComponent implements OnInit {
Now, you can open your browser to see the changes:
![Save button to the modal](images/bookstore-new-book-form-v2.png)
![Save button to the modal](./images/bookstore-new-book-form-v3.png)
## Updating a Book
@ -1041,7 +1038,7 @@ Open `/src/app/book/book.component.html`  and add the following `ngx-datatable-
aria-haspopup="true"
ngbDropdownToggle
>
<i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
<i class="fa fa-cog me-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button>
<div ngbDropdownMenu>
<button ngbDropdownItem (click)="editBook(row.id)">
@ -1055,7 +1052,7 @@ Open `/src/app/book/book.component.html`  and add the following `ngx-datatable-
Added an "Actions" dropdown as the first column of the table that is shown below:
![Action buttons](images/bookstore-actions-buttons.png)
![Action buttons](./images/bookstore-actions-buttons-2.png)
Also, change the `ng-template #abpHeader` section as shown below:
@ -1119,11 +1116,11 @@ Open `/src/app/book/book.component.html` and modify the `ngbDropdownMenu` to add
The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png)
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown-2.png)
Clicking the "Delete" action calls the `delete` method which then shows a confirmation popup as shown below:
![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png)
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup-2.png)
{{end}}
@ -1153,7 +1150,7 @@ Open the `Books.razor` and replace the `<CardHeader>` section with the following
This will change the card header by adding a "New book" button to the right side:
![blazor-add-book-button](images/blazor-add-book-button.png)
![blazor-add-book-button](./images/blazor-add-book-button-2.png)
Now, we can add a modal that will be opened when we click the button.
@ -1188,7 +1185,7 @@ Open the `Books.razor` and add the following code to the end of the page:
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"]
@L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem>
}
</Select>
@ -1227,7 +1224,7 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper<T>` at
That's all. Run the application and try to add a new book:
![blazor-new-book-modal](images/blazor-new-book-modal.png)
![blazor-new-book-modal](./images/blazor-new-book-modal-2.png)
## Updating a Book
@ -1253,7 +1250,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti
The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:
![blazor-edit-book-action](images/blazor-edit-book-action-2.png)
![blazor-edit-book-action](./images/blazor-edit-book-action-3.png)
### Edit Modal
@ -1286,7 +1283,7 @@ We can now define a modal to edit the book. Add the following code to the end of
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"]
@L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem>
}
</Select>
@ -1324,14 +1321,13 @@ Open the `BookStoreBlazorAutoMapperProfile` inside the `Acme.BookStore.Blazor` p
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Blazor
namespace Acme.BookStore.Blazor;
public class BookStoreBlazorAutoMapperProfile : Profile
{
public class BookStoreBlazorAutoMapperProfile : Profile
public BookStoreBlazorAutoMapperProfile()
{
public BookStoreBlazorAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
CreateMap<BookDto, CreateUpdateBookDto>();
}
}
````
@ -1342,7 +1338,7 @@ namespace Acme.BookStore.Blazor
You can now run the application and try to edit a book.
![blazor-edit-book-modal](images/blazor-edit-book-modal.png)
![blazor-edit-book-modal](./images/blazor-edit-book-modal-2.png)
> Tip: Try to leave the *Name* field empty and submit the form to show the validation error message.
@ -1363,7 +1359,7 @@ Open the `Books.razor` page and add the following `EntityAction` code under the
The "Actions" button becomes a dropdown since it has two actions now:
![blazor-edit-book-action](images/blazor-delete-book-action.png)
![blazor-delete-book-action](./images/blazor-delete-book-action-2.png)
Run the application and try to delete a book.
@ -1423,7 +1419,7 @@ Here's the complete code to create the book management CRUD page, that has been
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType.{Enum.GetName(context.Type)}"]
@L[$"Enum:BookType.{context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
@ -1475,7 +1471,7 @@ Here's the complete code to create the book management CRUD page, that has been
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"]
@L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem>
}
</Select>
@ -1528,7 +1524,7 @@ Here's the complete code to create the book management CRUD page, that has been
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"]
@L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem>
}
</Select>

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

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
{{if UI == "MVC" && DB == "EF"}}
@ -88,30 +85,31 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit;
namespace Acme.BookStore.Books
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
namespace Acme.BookStore.Books;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
{{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]
{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
}
````
@ -179,69 +177,70 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit;
namespace Acme.BookStore.Books
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
namespace Acme.BookStore.Books;
{{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]
{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
private readonly IBookAppService _bookAppService;
_bookAppService = GetRequiredService<IBookAppService>();
}
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
}
[Fact]
public async Task Should_Create_A_Valid_Book()
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
//Act
var result = await _bookAppService.CreateAsync(
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
}
````

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

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
{{if UI == "MVC" && DB == "EF"}}
@ -58,19 +55,18 @@ A permission must have a unique name (a `string`). The best way is to define it
Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below:
````csharp
namespace Acme.BookStore.Permissions
namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{
public static class BookStorePermissions
public const string GroupName = "BookStore";
public static class Books
{
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
````
@ -88,24 +84,23 @@ using Acme.BookStore.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace Acme.BookStore.Permissions
namespace Acme.BookStore.Permissions;
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
public override void Define(IPermissionDefinitionContext context)
{
public override void Define(IPermissionDefinitionContext context)
{
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
````
@ -130,7 +125,7 @@ Once you define the permissions, you can see them on the **permission management
Go to the *Administration -> Identity -> Roles* page, select *Permissions* action for the admin role to open the permission management modal:
![bookstore-permissions-ui](images/bookstore-permissions-ui.png)
![bookstore-permissions-ui](images/bookstore-permissions-ui-2.png)
Grant the permissions you want and save the modal.
@ -151,26 +146,25 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
namespace Acme.BookStore.Books;
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
}
````
@ -202,7 +196,7 @@ Now, unauthorized users are redirected to the **login page**.
The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission.
![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png)
![bookstore-new-book-button-small](images/bookstore-new-book-button-small-2.png)
Open the `Pages/Books/Index.cshtml` file and change the content as shown below:
@ -251,7 +245,7 @@ Open the `Pages/Books/Index.cshtml` file and change the content as shown below:
Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions-2.png)
We should hide an action if the current user has not granted for the related permission. Datatables row actions has a `visible` option that can be set to `false` to hide the action item.
@ -301,82 +295,22 @@ context.Menu.AddItem(
And replace this code block with the following:
````csharp
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
).RequirePermissions(BookStorePermissions.Books.Default) // Check the permission!
)
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
));
}
````
You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return values. The final `BookStoreMenuContributor` class should be the following:
````csharp
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Acme.BookStore.Localization;
using Acme.BookStore.MultiTenancy;
using Acme.BookStore.Permissions;
using Volo.Abp.TenantManagement.Web.Navigation;
using Volo.Abp.UI.Navigation;
namespace Acme.BookStore.Web.Menus
{
public class BookStoreMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
if (!MultiTenancyConsts.IsEnabled)
{
var administration = context.Menu.GetAdministration();
administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
}
var l = context.GetLocalizer<BookStoreResource>();
context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/"));
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
));
}
}
}
}
````
We've only added the `.RequirePermissions(BookStorePermissions.Books.Default)` extension method call for the inner menu item.
{{else if UI == "NG"}}
@ -429,7 +363,7 @@ Open the `/src/app/book/book.component.html` file and replace the create button
````html
<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Create'" id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<i class="fa fa-plus me-1"></i>
<span>{%{{{ '::NewBook' | abpLocalization }}}%}</span>
</button>
````
@ -440,7 +374,7 @@ Open the `/src/app/book/book.component.html` file and replace the create button
Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions-2.png)
We should hide an action if the current user has not granted for the related permission.

202
docs/en/Tutorials/Part-6.md

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
## Introduction
@ -63,45 +60,44 @@ using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class Author : FullAuditedAggregateRoot<Guid>
{
public class Author : FullAuditedAggregateRoot<Guid>
{
public string Name { get; private set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
public string Name { get; private set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
private Author()
{
/* This constructor is for deserialization / ORM purpose */
}
private Author()
{
/* This constructor is for deserialization / ORM purpose */
}
internal Author(
Guid id,
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
: base(id)
{
SetName(name);
BirthDate = birthDate;
ShortBio = shortBio;
}
internal Author(
Guid id,
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
: base(id)
{
SetName(name);
BirthDate = birthDate;
ShortBio = shortBio;
}
internal Author ChangeName([NotNull] string name)
{
SetName(name);
return this;
}
internal Author ChangeName([NotNull] string name)
{
SetName(name);
return this;
}
private void SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: AuthorConsts.MaxNameLength
);
}
private void SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: AuthorConsts.MaxNameLength
);
}
}
````
@ -116,13 +112,13 @@ namespace Acme.BookStore.Authors
`AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project:
````csharp
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public static class AuthorConsts
{
public static class AuthorConsts
{
public const int MaxNameLength = 64;
}
public const int MaxNameLength = 64;
}
````
Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later.
@ -138,53 +134,52 @@ using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Services;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class AuthorManager : DomainService
{
public class AuthorManager : DomainService
private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{
private readonly IAuthorRepository _authorRepository;
_authorRepository = authorRepository;
}
public AuthorManager(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<Author> CreateAsync(
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
public async Task<Author> CreateAsync(
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
throw new AuthorAlreadyExistsException(name);
}
public async Task ChangeNameAsync(
[NotNull] Author author,
[NotNull] string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
}
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
public async Task ChangeNameAsync(
[NotNull] Author author,
[NotNull] string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
author.ChangeName(newName);
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
author.ChangeName(newName);
}
}
````
@ -198,15 +193,14 @@ Both methods checks if there is already an author with the given name and throws
````csharp
using Volo.Abp;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class AuthorAlreadyExistsException : BusinessException
{
public class AuthorAlreadyExistsException : BusinessException
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
WithData("name", name);
}
}
````
@ -216,12 +210,11 @@ namespace Acme.BookStore.Authors
Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below:
````csharp
namespace Acme.BookStore
namespace Acme.BookStore;
public static class BookStoreDomainErrorCodes
{
public static class BookStoreDomainErrorCodes
{
public const string AuthorAlreadyExists = "BookStore:00001";
}
public const string AuthorAlreadyExists = "BookStore:00001";
}
````
@ -243,19 +236,18 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
namespace Acme.BookStore.Authors;
Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
````
@ -265,7 +257,7 @@ namespace Acme.BookStore.Authors
We will implement this repository in the next part.
> Both of these methods might **seem unnecessary** since the standard repositories already `IQueryable` and you can directly use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it.
> Both of these methods might **seem unnecessary** since the standard repositories already provide generic querying methods and you can easily use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it.
## Conclusion

139
docs/en/Tutorials/Part-7.md

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
## Introduction
@ -93,7 +90,7 @@ You can apply changes to the database using the following command, in the same c
dotnet ef database update
````
> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{else if DB=="Mongo"}}
@ -124,41 +121,40 @@ using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class EfCoreAuthorRepository
: EfCoreRepository<BookStoreDbContext, Author, Guid>,
IAuthorRepository
{
public class EfCoreAuthorRepository
: EfCoreRepository<BookStoreDbContext, Author, Guid>,
IAuthorRepository
public EfCoreAuthorRepository(
IDbContextProvider<BookStoreDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{
public EfCoreAuthorRepository(
IDbContextProvider<BookStoreDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
````
@ -185,42 +181,41 @@ using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class MongoDbAuthorRepository
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>,
IAuthorRepository
{
public class MongoDbAuthorRepository
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>,
IAuthorRepository
public MongoDbAuthorRepository(
IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider
) : base(dbContextProvider)
{
public MongoDbAuthorRepository(
IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider
) : base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{
var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var queryable = await GetMongoQueryableAsync();
return await queryable
.WhereIf<Author, IMongoQueryable<Author>>(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.As<IMongoQueryable<Author>>()
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
public async Task<Author> FindByNameAsync(string name)
{
var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var queryable = await GetMongoQueryableAsync();
return await queryable
.WhereIf<Author, IMongoQueryable<Author>>(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.As<IMongoQueryable<Author>>()
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
```

387
docs/en/Tutorials/Part-8.md

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
## Introduction
@ -53,20 +50,19 @@ using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public interface IAuthorAppService : IApplicationService
{
public interface IAuthorAppService : IApplicationService
{
Task<AuthorDto> GetAsync(Guid id);
Task<AuthorDto> GetAsync(Guid id);
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
Task<AuthorDto> CreateAsync(CreateAuthorDto input);
Task<AuthorDto> CreateAsync(CreateAuthorDto input);
Task UpdateAsync(Guid id, UpdateAuthorDto input);
Task UpdateAsync(Guid id, UpdateAuthorDto input);
Task DeleteAsync(Guid id);
}
Task DeleteAsync(Guid id);
}
````
@ -83,16 +79,15 @@ This interface is using the DTOs defined below (create them for your project).
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class AuthorDto : EntityDto<Guid>
{
public class AuthorDto : EntityDto<Guid>
{
public string Name { get; set; }
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
public string ShortBio { get; set; }
}
````
@ -103,12 +98,11 @@ namespace Acme.BookStore.Authors
````csharp
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors
namespace Acme.BookStore.Authors;
public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
}
public string Filter { get; set; }
}
````
@ -123,19 +117,18 @@ namespace Acme.BookStore.Authors
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors
{
public class CreateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
namespace Acme.BookStore.Authors;
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
public class CreateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
````
@ -147,19 +140,18 @@ Data annotation attributes can be used to validate the DTO. See the [validation
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors
{
public class UpdateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
namespace Acme.BookStore.Authors;
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
public class UpdateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
````
@ -179,24 +171,23 @@ using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
[Authorize(BookStorePermissions.Authors.Default)]
public class AuthorAppService : BookStoreAppService, IAuthorAppService
{
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
namespace Acme.BookStore.Authors;
public AuthorAppService(
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_authorRepository = authorRepository;
_authorManager = authorManager;
}
[Authorize(BookStorePermissions.Authors.Default)]
public class AuthorAppService : BookStoreAppService, IAuthorAppService
{
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
//...SERVICE METHODS WILL COME HERE...
public AuthorAppService(
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_authorRepository = authorRepository;
_authorManager = authorManager;
}
//...SERVICE METHODS WILL COME HERE...
}
````
@ -330,28 +321,27 @@ You can't compile the code since it is expecting some constants declared in the
Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below:
````csharp
namespace Acme.BookStore.Permissions
namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
// *** ADDED a NEW NESTED CLASS ***
public static class Authors
{
public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
// *** ADDED a NEW NESTED CLASS ***
public static class Authors
{
public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
````
@ -361,13 +351,10 @@ Then open the `BookStorePermissionDefinitionProvider` in the same project and ad
````csharp
var authorsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Authors.Default, L("Permission:Authors"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));
````
@ -406,72 +393,71 @@ using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
// ADDED SEED DATA FOR AUTHORS
// ADDED SEED DATA FOR AUTHORS
if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
}
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
}
}
}
@ -497,73 +483,74 @@ using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Acme.BookStore.Authors
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase
namespace Acme.BookStore.Authors;
{{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]
{{end}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase
{
private readonly IAuthorAppService _authorAppService;
public AuthorAppService_Tests()
{
private readonly IAuthorAppService _authorAppService;
_authorAppService = GetRequiredService<IAuthorAppService>();
}
public AuthorAppService_Tests()
{
_authorAppService = GetRequiredService<IAuthorAppService>();
}
[Fact]
public async Task Should_Get_All_Authors_Without_Any_Filter()
{
var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
[Fact]
public async Task Should_Get_All_Authors_Without_Any_Filter()
{
var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldContain(author => author.Name == "Douglas Adams");
}
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Get_Filtered_Authors()
{
var result = await _authorAppService.GetListAsync(
new GetAuthorListDto {Filter = "George"});
[Fact]
public async Task Should_Get_Filtered_Authors()
{
var result = await _authorAppService.GetListAsync(
new GetAuthorListDto {Filter = "George"});
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
}
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Create_A_New_Author()
{
var authorDto = await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Edward Bellamy",
BirthDate = new DateTime(1850, 05, 22),
ShortBio = "Edward Bellamy was an American author..."
}
);
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact]
public async Task Should_Create_A_New_Author()
[Fact]
public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{
var authorDto = await _authorAppService.CreateAsync(
await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Edward Bellamy",
BirthDate = new DateTime(1850, 05, 22),
ShortBio = "Edward Bellamy was an American author..."
Name = "Douglas Adams",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact]
public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{
await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Douglas Adams",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
});
}
//TODO: Test other methods...
});
}
//TODO: Test other methods...
}
````

458
docs/en/Tutorials/Part-9.md

@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer
* [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md).
## Introduction
@ -98,14 +95,13 @@ This is a simple page similar to the Books page we had created before. It import
````csharp
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Authors
namespace Acme.BookStore.Web.Pages.Authors;
public class IndexModel : PageModel
{
public class IndexModel : PageModel
public void OnGet()
{
public void OnGet()
{
}
}
}
````
@ -223,28 +219,39 @@ Notice that we've added more keys. They will be used in the next sections.
### Add to the Main Menu
Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add the following code in the end of the `ConfigureMainMenuAsync` method:
Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add a new *Authors* menu item under the *Book Store* menu item. The following code (in the `ConfigureMainMenuAsync` method) shows the final code part:
````csharp
if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Authors",
l["Menu:Authors"],
url: "/Authors"
));
}
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
).RequirePermissions(BookStorePermissions.Books.Default)
).AddItem( // ADDED THE NEW "AUTHORS" MENU ITEM UNDER THE "BOOK STORE" MENU
new ApplicationMenuItem(
"BooksStore.Authors",
l["Menu:Authors"],
url: "/Authors"
).RequirePermissions(BookStorePermissions.Books.Default)
)
);
````
### Run the Application
Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions.png)
![bookstore-author-permissions](images/bookstore-author-permissions-3.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-authors-page.png)
![bookstore-authors-page](images/bookstore-authors-page-3.png)
The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet.
@ -294,45 +301,44 @@ using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors
namespace Acme.BookStore.Web.Pages.Authors;
public class CreateModalModel : BookStorePageModel
{
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateAuthorViewModel Author { get; set; }
[BindProperty]
public CreateAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService;
private readonly IAuthorAppService _authorAppService;
public CreateModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public CreateModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public void OnGet()
{
Author = new CreateAuthorViewModel();
}
public void OnGet()
{
Author = new CreateAuthorViewModel();
}
public async Task<IActionResult> OnPostAsync()
{
var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author);
await _authorAppService.CreateAsync(dto);
return NoContent();
}
public async Task<IActionResult> OnPostAsync()
{
var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author);
await _authorAppService.CreateAsync(dto);
return NoContent();
}
public class CreateAuthorViewModel
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
public class CreateAuthorViewModel
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[TextArea]
public string ShortBio { get; set; }
}
[TextArea]
public string ShortBio { get; set; }
}
}
```
@ -351,25 +357,24 @@ using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Web
namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{
public class BookStoreWebAutoMapperProfile : Profile
public BookStoreWebAutoMapperProfile()
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
CreateMap<BookDto, CreateUpdateBookDto>();
// ADD a NEW MAPPING
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
}
// ADD a NEW MAPPING
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
}
}
````
"New author" button will work as expected and open a new model when you run the application again:
![bookstore-new-author-modal](images/bookstore-new-author-modal.png)
![bookstore-new-author-modal](images/bookstore-new-author-modal-2.png)
## Edit Modal
@ -412,52 +417,51 @@ using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors
namespace Acme.BookStore.Web.Pages.Authors;
public class EditModalModel : BookStorePageModel
{
public class EditModalModel : BookStorePageModel
{
[BindProperty]
public EditAuthorViewModel Author { get; set; }
[BindProperty]
public EditAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService;
private readonly IAuthorAppService _authorAppService;
public EditModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public EditModalModel(IAuthorAppService authorAppService)
{
_authorAppService = authorAppService;
}
public async Task OnGetAsync(Guid id)
{
var authorDto = await _authorAppService.GetAsync(id);
Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto);
}
public async Task OnGetAsync(Guid id)
{
var authorDto = await _authorAppService.GetAsync(id);
Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto);
}
public async Task<IActionResult> OnPostAsync()
{
await _authorAppService.UpdateAsync(
Author.Id,
ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author)
);
public async Task<IActionResult> OnPostAsync()
{
await _authorAppService.UpdateAsync(
Author.Id,
ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author)
);
return NoContent();
}
return NoContent();
}
public class EditAuthorViewModel
{
[HiddenInput]
public Guid Id { get; set; }
public class EditAuthorViewModel
{
[HiddenInput]
public Guid Id { get; set; }
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[TextArea]
public string ShortBio { get; set; }
}
[TextArea]
public string ShortBio { get; set; }
}
}
```
@ -474,22 +478,21 @@ using Acme.BookStore.Authors;
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Web
namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{
public class BookStoreWebAutoMapperProfile : Profile
public BookStoreWebAutoMapperProfile()
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
CreateMap<BookDto, CreateUpdateBookDto>();
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>();
// ADD THESE NEW MAPPINGS
CreateMap<AuthorDto, Pages.Authors.EditModalModel.EditAuthorViewModel>();
CreateMap<Pages.Authors.EditModalModel.EditAuthorViewModel,
UpdateAuthorDto>();
}
// ADD THESE NEW MAPPINGS
CreateMap<AuthorDto, Pages.Authors.EditModalModel.EditAuthorViewModel>();
CreateMap<Pages.Authors.EditModalModel.EditAuthorViewModel,
UpdateAuthorDto>();
}
}
```
@ -725,7 +728,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo
<div class="text-end col col-md-6">
<div class="text-lg-end pt-2">
<button *abpPermission="'BookStore.Authors.Create'" id="create" class="btn btn-primary" type="button" (click)="createAuthor()">
<i class="fa fa-plus mr-1"></i>
<i class="fa fa-plus me-1"></i>
<span>{%{{{ '::NewAuthor' | abpLocalization }}}%}</span>
</button>
</div>
@ -747,7 +750,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo
aria-haspopup="true"
ngbDropdownToggle
>
<i class="fa fa-cog mr-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
<i class="fa fa-cog me-1"></i>{%{{{ '::Actions' | abpLocalization }}}%}
</button>
<div ngbDropdownMenu>
<button *abpPermission="'BookStore.Authors.Edit'" ngbDropdownItem (click)="editAuthor(row.id)">
@ -782,7 +785,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo
<input type="text" id="author-name" class="form-control" formControlName="name" autofocus />
</div>
<div class="form-group">
<div class="mt-2">
<label>Birth date</label><span> * </span>
<input
#datepicker="ngbDatepicker"
@ -825,11 +828,11 @@ This page uses some localization keys we need to declare. Open the `en.json` fil
Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions.png)
![bookstore-author-permissions](images/bookstore-author-permissions-2.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-angular-authors-page.png)
![bookstore-authors-page](images/bookstore-angular-authors-page-2.png)
That's all! This is a fully working CRUD page, you can create, edit and delete authors.
@ -855,12 +858,11 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
<Card>
<CardHeader>
<Row>
<Column ColumnSize="ColumnSize.Is6">
<Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Authors"]</h2>
</Column>
<Column ColumnSize="ColumnSize.Is6">
<Paragraph Alignment="TextAlignment.Right">
<Column ColumnSize="ColumnSize.IsAuto">
@if (CanCreateAuthor)
{
<Button Color="Color.Primary"
@ -868,7 +870,6 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor
@L["NewAuthor"]
</Button>
}
</Paragraph>
</Column>
</Row>
</CardHeader>
@ -1046,142 +1047,141 @@ using Blazorise.DataGrid;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Blazor.Pages
namespace Acme.BookStore.Blazor.Pages;
public partial class Authors
{
public partial class Authors
{
private IReadOnlyList<AuthorDto> AuthorList { get; set; }
private IReadOnlyList<AuthorDto> AuthorList { get; set; }
private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
private int CurrentPage { get; set; }
private string CurrentSorting { get; set; }
private int TotalCount { get; set; }
private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
private int CurrentPage { get; set; }
private string CurrentSorting { get; set; }
private int TotalCount { get; set; }
private bool CanCreateAuthor { get; set; }
private bool CanEditAuthor { get; set; }
private bool CanDeleteAuthor { get; set; }
private bool CanCreateAuthor { get; set; }
private bool CanEditAuthor { get; set; }
private bool CanDeleteAuthor { get; set; }
private CreateAuthorDto NewAuthor { get; set; }
private CreateAuthorDto NewAuthor { get; set; }
private Guid EditingAuthorId { get; set; }
private UpdateAuthorDto EditingAuthor { get; set; }
private Guid EditingAuthorId { get; set; }
private UpdateAuthorDto EditingAuthor { get; set; }
private Modal CreateAuthorModal { get; set; }
private Modal EditAuthorModal { get; set; }
private Modal CreateAuthorModal { get; set; }
private Modal EditAuthorModal { get; set; }
private Validations CreateValidationsRef;
private Validations EditValidationsRef;
public Authors()
{
NewAuthor = new CreateAuthorDto();
EditingAuthor = new UpdateAuthorDto();
}
private Validations CreateValidationsRef;
private Validations EditValidationsRef;
public Authors()
{
NewAuthor = new CreateAuthorDto();
EditingAuthor = new UpdateAuthorDto();
}
protected override async Task OnInitializedAsync()
{
await SetPermissionsAsync();
await GetAuthorsAsync();
}
protected override async Task OnInitializedAsync()
{
await SetPermissionsAsync();
await GetAuthorsAsync();
}
private async Task SetPermissionsAsync()
{
CanCreateAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Create);
private async Task SetPermissionsAsync()
{
CanCreateAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Create);
CanEditAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Edit);
CanEditAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Edit);
CanDeleteAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Delete);
}
CanDeleteAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Delete);
}
private async Task GetAuthorsAsync()
{
var result = await AuthorAppService.GetListAsync(
new GetAuthorListDto
{
MaxResultCount = PageSize,
SkipCount = CurrentPage * PageSize,
Sorting = CurrentSorting
}
);
private async Task GetAuthorsAsync()
{
var result = await AuthorAppService.GetListAsync(
new GetAuthorListDto
{
MaxResultCount = PageSize,
SkipCount = CurrentPage * PageSize,
Sorting = CurrentSorting
}
);
AuthorList = result.Items;
TotalCount = (int)result.TotalCount;
}
AuthorList = result.Items;
TotalCount = (int)result.TotalCount;
}
private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<AuthorDto> e)
{
CurrentSorting = e.Columns
.Where(c => c.SortDirection != SortDirection.Default)
.Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : ""))
.JoinAsString(",");
CurrentPage = e.Page - 1;
private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<AuthorDto> e)
{
CurrentSorting = e.Columns
.Where(c => c.SortDirection != SortDirection.Default)
.Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : ""))
.JoinAsString(",");
CurrentPage = e.Page - 1;
await GetAuthorsAsync();
await GetAuthorsAsync();
await InvokeAsync(StateHasChanged);
}
await InvokeAsync(StateHasChanged);
}
private void OpenCreateAuthorModal()
{
CreateValidationsRef.ClearAll();
NewAuthor = new CreateAuthorDto();
CreateAuthorModal.Show();
}
private void OpenCreateAuthorModal()
{
CreateValidationsRef.ClearAll();
NewAuthor = new CreateAuthorDto();
CreateAuthorModal.Show();
}
private void CloseCreateAuthorModal()
{
CreateAuthorModal.Hide();
}
private void CloseCreateAuthorModal()
{
CreateAuthorModal.Hide();
}
private void OpenEditAuthorModal(AuthorDto author)
{
EditValidationsRef.ClearAll();
EditingAuthorId = author.Id;
EditingAuthor = ObjectMapper.Map<AuthorDto, UpdateAuthorDto>(author);
EditAuthorModal.Show();
}
private void OpenEditAuthorModal(AuthorDto author)
{
EditValidationsRef.ClearAll();
EditingAuthorId = author.Id;
EditingAuthor = ObjectMapper.Map<AuthorDto, UpdateAuthorDto>(author);
EditAuthorModal.Show();
}
private async Task DeleteAuthorAsync(AuthorDto author)
private async Task DeleteAuthorAsync(AuthorDto author)
{
var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name];
if (!await Message.Confirm(confirmMessage))
{
var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name];
if (!await Message.Confirm(confirmMessage))
{
return;
}
await AuthorAppService.DeleteAsync(author.Id);
await GetAuthorsAsync();
return;
}
private void CloseEditAuthorModal()
{
EditAuthorModal.Hide();
}
await AuthorAppService.DeleteAsync(author.Id);
await GetAuthorsAsync();
}
private void CloseEditAuthorModal()
{
EditAuthorModal.Hide();
}
private async Task CreateAuthorAsync()
private async Task CreateAuthorAsync()
{
if (await CreateValidationsRef.ValidateAll())
{
if (await CreateValidationsRef.ValidateAll())
{
await AuthorAppService.CreateAsync(NewAuthor);
await GetAuthorsAsync();
CreateAuthorModal.Hide();
}
await AuthorAppService.CreateAsync(NewAuthor);
await GetAuthorsAsync();
CreateAuthorModal.Hide();
}
}
private async Task UpdateAuthorAsync()
private async Task UpdateAuthorAsync()
{
if (await EditValidationsRef.ValidateAll())
{
if (await EditValidationsRef.ValidateAll())
{
await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor);
await GetAuthorsAsync();
EditAuthorModal.Hide();
}
await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor);
await GetAuthorsAsync();
EditAuthorModal.Hide();
}
}
}
@ -1232,11 +1232,11 @@ We should complete the localizations we've used above. Open the `en.json` file u
Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions.png)
![bookstore-author-permissions](images/bookstore-author-permissions-2.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-authors-blazor-ui.png)
![bookstore-authors-page](images/bookstore-authors-page-3.png)
That's all! This is a fully working CRUD page, you can create, edit and delete the authors.

36
docs/en/Tutorials/Todo/Index.md

@ -14,6 +14,42 @@ This is a single-part quick-start tutorial to build a simple todo application wi
You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp).
This documentation has a video tutorial on **YouTube**!! You can watch it here:
{{if UI=="MVC" && DB =="EF"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/763DV0fwSbk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="Blazor" && DB=="EF"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/ivxJsi8c7-8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="BlazorServer" && DB=="EF"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/1BdYg5NLrJs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="NG" && DB=="EF"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/Lqh1j1H5pkg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="MVC" && DB=="Mongo"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/7Rm-K2re4MI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="BlazorServer" && DB=="Mongo"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/i23C8hN7OAs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="Blazor" && DB=="Mongo"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/JpiMiXOBG6A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{else if UI=="NG" && DB=="Mongo"}}
<iframe width="560" height="315" src="https://www.youtube.com/embed/DOxk9Doxad0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{{end}}
## Pre-Requirements
* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 7.0+](https://dotnet.microsoft.com/download/dotnet) development.

BIN
docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 256 KiB

BIN
docs/en/Tutorials/Todo/todo-list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Tutorials/Todo/todo-ui-initial.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Tutorials/images/blazor-add-book-button-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
docs/en/Tutorials/images/blazor-delete-book-action-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
docs/en/Tutorials/images/blazor-edit-book-action-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/en/Tutorials/images/blazor-edit-book-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
docs/en/Tutorials/images/blazor-new-book-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/en/Tutorials/images/book-create-modal-with-author-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/en/Tutorials/images/bookstore-added-author-to-book-list-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/en/Tutorials/images/bookstore-added-authors-to-modals-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-author-selection-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-authors-page-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
docs/en/Tutorials/images/bookstore-author-permissions-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/en/Tutorials/images/bookstore-author-permissions-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/en/Tutorials/images/bookstore-authors-page-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/en/Tutorials/images/bookstore-authors-page-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/en/Tutorials/images/bookstore-create-dialog-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Tutorials/images/bookstore-new-author-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
docs/en/Tutorials/images/bookstore-new-book-form-v3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/en/Tutorials/images/bookstore-new-menu-item-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

9
docs/en/UI/Angular/Account-Module.md

@ -61,16 +61,23 @@ npm install @volo/abp.ng.account
Open the `app.module.ts` and add `AccountPublicConfigModule.forRoot()` to the imports array as shown below:
> Ensure that the `Account Layout Module` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.` when you try to access the account pages. Otherwise, you can skip adding the `AccountLayoutModule` step.
```js
// app.module.ts
import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
// if you are using or want to use Lepton X, you should add AccountLayoutModule
// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account'
//...
@NgModule({
imports: [
//...
AccountPublicConfigModule.forRoot()
AccountPublicConfigModule.forRoot(),
// AccountLayoutModule.forRoot() // Only for Lepton X
],
//...
})

21
docs/en/UI/Angular/OAuth-Module.md

@ -0,0 +1,21 @@
# ABP OAuth Package
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0.
If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot()" in your app.module.ts as an import after "CoreModule.forRoot(...)".
Those abstractions can be found in the @abp/ng-core packages.
- `AuthService` (the class that implements the IAuthService interface).
- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
- `AuthGuard` (the class that implements the IAuthGuard interface).
- `ApiInterceptor` (the class that implements the IApiInterceptor interface).
Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule.
- `PIPE_TO_LOGIN_FN_KEY` a provide that calls a function when the user is not authenticated. The function should be PipeToLoginFn type.
- `SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY` a provide that calls a function when the user is authenticated. The function should be SetTokenResponseToStorageFn type.
- `CHECK_AUTHENTICATION_STATE_FN_KEY` a provide that calls a function when the user is authenticated and stores the auth state. The function should be CheckAuthenticationStateFn type.
The tokens and interfaces are in the `@abp/ng.core` package but the implementation of these interfaces is in the `@abp/ng.oauth` package.
If you want to make your own authentication system, you must also change these 'abstract' classes.
ApiInterceptor is provided by `@abp/ng.core` but overridden with `@abp/ng.oauth`. The ApiInterceptor adds the token, accepted-language, and tenant id to the header of the HTTP request. It also calls the http-wait service.

14
docs/en/UI/Angular/Quick-Start.md

@ -37,19 +37,9 @@ This command will prepare a solution with an Angular and a .NET Core project in
To continue reading without checking other methods, visit [Angular project structure section](#angular-project-structure).
### 2. Direct Download
### 2. Generating a CLI Command from Get Started Page
You may [download a solution scaffold directly on ABP.io](https://abp.io/get-started) if you are more comfortable with GUI or simply want to try ABP without installing the CLI.
Please do the following:
1. Click on the "DIRECT DOWNLOAD" tab.
2. Fill out the short form about your project.
3. Click on the "Create now" button.
...and a customized download will start in a few seconds.
To avoid IDE warnings, run `yarn` or `npm install` in the Angular root folder before you continue to the next section. It is not imperative but recommended.
You can generate a CLI command on the [get started page of the abp.io website](https://abp.io/get-started). Then, use the command on your terminal to create a new [Startup Template](../../Startup-Templates/Index.md).
## Angular Project Structure

23
docs/en/UI/Angular/Theme-Configurations.md

@ -43,6 +43,16 @@ Theme packages no longer import styles as CSS modules as of ABP version 6.0. The
"inject": false,
"bundleName": "abp-bundle.rtl"
},
{
"input":"node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.rtl.css",
"inject":false,
"bundleName":"font-bundle.rtl"
}
,{
"input":"node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.css",
"inject":false,
"bundleName":"font-bundle"
},
```
## Theme Basic
@ -150,7 +160,18 @@ Theme packages no longer import styles as CSS modules as of ABP version 6.0. The
"input": "node_modules/@volosoft/abp.ng.theme.lepton-x/assets/css/abp-bundle.rtl.css",
"inject": false,
"bundleName": "abp-bundle.rtl"
}
},
{
"input": "node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.css",
"inject": false,
"bundleName": "font-bundle"
},
{
"input": "node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.rtl.css",
"inject": false,
"bundleName": "font-bundle.rtl"
},
```
## Theme Lepton

5
docs/en/UI/AspNetCore/AutoComplete-Select.md

@ -22,7 +22,8 @@ A simple usage is presented below.
data-autocomplete-display-property="name"
data-autocomplete-value-property="id"
data-autocomplete-items-property="items"
data-autocomplete-filter-param-name="filter">
data-autocomplete-filter-param-name="filter"
data-autocomplete-allow-clear="true">
<!-- You can define selected option(s) here -->
<option selected value="@SelectedAuthor.Id">@SelectedAuthor.Name</option>
@ -38,6 +39,8 @@ The select must have the `auto-complete-select` class and the following attribut
- `data-autocomplete-filter-param-name`: * Filter text property name. _(For example: `filter`)_.
- `data-autocomplete-selected-item-name`: Text to display as selected item.
- `data-autocomplete-parent-selector`: jQuery selector expression for parent DOM. _(If it's in a modal, it's suggested to send the modal selector as this parameter)_.
- `data-autocomplete-allow-clear`: If `true`, it'll allow to clear the selected value. Default value: `false`.
- `data-autocomplete-placeholder`: Placeholder text to display when no value is selected.
Also, selected value(s) should be defined with the `<option>` tags inside select, since pagination is applied and the selected options might haven't loaded yet.

21
docs/en/UI/AspNetCore/Forms-Validation.md

@ -201,6 +201,27 @@ In this case, you can add an entry to the localization file using the key `MyNam
> If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system.
### Enum Localization
Enum members are also automatically localized wherever possible. For example, when we added `<abp-select asp-for="Movie.Genre"/>` to the form (like we did in the *ABP Form Tag Helpers* section), ABP can automatically fill the localized names of Enum members. To enabled it, you should define the localized values in your localization JSON file. Example entries for the `Genre` Enum defined in the *ABP Form Tag Helpers* section:
````json
"Enum:Genre.0": "Classic movie",
"Enum:Genre.1": "Action movie",
"Enum:Genre.2": "Fiction",
"Enum:Genre.3": "Fantasy",
"Enum:Genre.4": "Animation/Cartoon"
````
You can use one of the following syntaxes for the localization keys:
* `Enum:<enum-type-name>.<enum-value>`
* `<enum-type-name>.<enum-value>`
> Remember that if you don't specify values for your Enum, the values will be ordered, starting from `0`.
> MVC tag helpers also support using Enum member names instead of values (so, you can define `"Enum:Genre.Action"` instead of `"Enum:Genre.1"`, for example), but it is not suggested. Because, when you serialize Enum properties to JSON and send to clients, default serializer uses Enum values instead of Enum names. So, the Enum name won't be available to clients, and it will be a problem if you want to use the same localization values on the client side.
## See Also
* [Server Side Validation](../../Validation.md)

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

Loading…
Cancel
Save