diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..5ecb45e142 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +### Description + +Resolves #xxxx (write the related issue number if available) + +TODO: Describe what this PR has changed, add screenshot or animated GIF if available, write if it is a breaking change, and how to fix the breaking changes for existing applications if so. + +### Checklist + +- [ ] I fully tested it as developer / designer and created unit / integration tests +- [ ] I documented it (or no need to document or I will create a separate documentation issue) + +### How to test it? + +Please describe how this can be tested by the test engineers if it is not already explicit - or remove this section if no need to description. diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index 1a7b797ef8..1babddae5a 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch dev with rel-7.0 +name: Merge branch dev with rel-7.1 on: push: branches: - - rel-7.0 + - rel-7.1 permissions: contents: read jobs: - merge-dev-with-rel-6-0: + merge-dev-with-rel-7-1: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR @@ -18,13 +18,13 @@ jobs: ref: dev - name: Reset promotion branch run: | - git fetch origin rel-7.0:rel-7.0 - git reset --hard rel-7.0 + git fetch origin rel-7.1:rel-7.1 + git reset --hard rel-7.1 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-7-0/${{github.run_number}} - title: Merge branch dev with rel-7.0 - body: This PR generated automatically to merge dev with rel-7.0. Please review the changed files before merging to prevent any errors that may occur. + branch: auto-merge/rel-7-1/${{github.run_number}} + title: Merge branch dev with rel-7.1 + body: This PR generated automatically to merge dev with rel-7.1. Please review the changed files before merging to prevent any errors that may occur. reviewers: ${{github.actor}} token: ${{ github.token }} diff --git a/.github/workflows/image-compression.yml b/.github/workflows/image-compression.yml index 4556622e4a..9eef6db59b 100644 --- a/.github/workflows/image-compression.yml +++ b/.github/workflows/image-compression.yml @@ -6,6 +6,7 @@ on: - "**.jpeg" - "**.png" - "**.webp" + - "**.gif" types: - opened - synchronize diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml deleted file mode 100644 index d7716a9d7c..0000000000 --- a/.github/workflows/spellcheck.yml +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index ccb98f81a0..a386e86320 100644 --- a/.gitignore +++ b/.gitignore @@ -326,4 +326,6 @@ deploy/_run_all_log.txt # No commit yarn.lock files in the subfolders of templates directory -templates/**/yarn.lock \ No newline at end of file +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 diff --git a/Directory.Build.props b/Directory.Build.props index a66d3f5509..43a884f556 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ 7.0.0 - 7.0.0 + 7.0.1 7.0.0 @@ -17,7 +17,7 @@ 4.3.0 - 4.0.3 + 4.1.0 2.4.1 diff --git a/README.md b/README.md index 332cf2adc5..c3c7679aa3 100644 --- a/README.md +++ b/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) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index de2f917fc9..34d20a3bda 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -409,6 +409,49 @@ "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 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: 1MB
Supported file types: jpg, jpeg, png, SVG, WebP", + "UrlCode": "Url Code", + "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", + "AbpConsultantEdit": "Edit ABP Consultant", + "AbpConsultantCreate": "Create ABP Consultant", + "WhoWeAreItem": "Who We Are Item", + "FieldIsRequired": "{0} is required.", + "FieldIsNotValid": " {0} is not valid.", + "InterestedLicenseType": "Interested License Type" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index f73d317fad..246095c064 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -26,6 +26,8 @@ "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:030013": "A user with an active license cannot start a 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 +185,27 @@ "BlackFriday": "BLACK FRIDAY", "ValidForExistingCustomers": "Also valid for the
existing customers!", "CampaignBetweenDates": "From {0}
to {1}", - "SaveUpTo": "SAVE UP TO${0}K" + "SaveUpTo": "SAVE UP TO${0}K", + "ImplementingDDD": "Implementing Domain Driven Design", + "ExploreTheEBook": "Explore the E-Book", + "ExploreTheBook": "Explore the Book", + "ConsultantType": "Consultancy Type", + "Expert": "ABP Expert", + "Partner": "ABP Partner", + "Industry": "Industry", + "Location": "Location", + "Contact": "Contact", + "Partner_Year": "Partnership Year", + "Info": "Info", + "SpokenLanguages": "Spoken Languages", + "SocialMedia": "Social Media", + "Activity": "Activity", + "Type": "Type", + "Contribution": "Contribution", + "WhoWeAre": "Who We Are", + "Icons": "Icons", + "Url": "Url", + "Icon": "Icon", + "RecentActivities": "Recent Activities" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json index be07f9d832..b202302d8e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json +++ b/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": "انقر هنا للحصول على بريد إلكتروني للتأكيد إذا لم تكن قد حصلت عليه من قبل.", + "GetConfirmationEmail": "انقر هنا للحصول على بريد إلكتروني للتأكيد إذا لم تكن قد حصلت عليه من قبل.", "WhichLicenseTypeYouAreInterestedIn": "ما نوع الرخصة المهتم بها؟", "DontTakeOurWordForIt": "لا تأخذ كلمتنا لذلك...", "ReadAbpCommercialUsersWantYouToKnow": "اقرأ ما يريد مستخدمو ABP التجاري أن تعرفه", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 8a0e47c529..d4a60bde09 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -196,7 +196,7 @@ "ChangingDevelopers": "Can I change the registered developers of my organization in the future?", "ChangingDevelopersExplanation": "In addition to adding new developers to your license, you can also change the existing developers (you can remove a developer and add a new one to the same seat) without any additional cost.", "WhatHappensWhenLicenseEnds": "What happens when my license period ends?", - "WhatHappensWhenLicenseEndsExplanation1": "The ABP Commercial license is a perpetual license. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year update and support plan out of the box. In order to continue to get new features, performance enhancements, bug fixes, support and continue using ABP Suite, you need to renew your license. When your license expires, you will not get the following benefits:", + "WhatHappensWhenLicenseEndsExplanation1": "The ABP Commercial license is a perpetual license. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year update and support plan out of the box. In order to continue to get new features, performance enhancements, bug fixes, support and continue using ABP Suite, you need to renew your license. When your license expires;", "WhatHappensWhenLicenseEndsExplanation2": "You can not create new solutions using the ABP Commercial, but you can continue developing your existing applications forever.", "WhatHappensWhenLicenseEndsExplanation3": "You will be able to get updates for the modules and themes within your MINOR version (except RC or Preview versions). For example: if you are using v3.2.0 of a module, you can still get updates for v3.2.x (v3.2.1, v3.2.5... etc.) of that module. But you cannot get updates for the next major or minor version (like v3.3.0, v3.3.3, 4.x.x.. etc.). For example, when your license expired, the latest release was v4.4.3, and later, it published both 4.4.4 version and 4.5.0 version, you would be able to access the v4.4.X but you wouldn't be access the v4.5.X.", "WhatHappensWhenLicenseEndsExplanation4": "You can not install new modules and themes added to the ABP Commercial platform after your license ends.", @@ -369,7 +369,6 @@ "FreeTrial": "Free Trial", "AcceptsMarketingCommunications": " Yes, I`d like to receive ABP Commercial marketing communications.", "PurposeOfUsage": "Purpose of usage", - "Industry": "Industry", "Choose": "- Choose -", "CompanyOrganizationName": "Company / Organization name", "CompanySize": "Company size", @@ -735,7 +734,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": "Click here to get a verification email if you haven't got it before.", + "GetConfirmationEmail": "Click here to get a verification email 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 +805,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 info@abp.io 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" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json index 8d1c67efae..b144988670 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json +++ b/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": "Kattintson ide, ha megerősítő e-mailt szeretne kapni, ha még nem kapta meg.", + "GetConfirmationEmail": "Kattintson ide, ha megerősítő e-mailt szeretne kapni, 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", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json index ae7a1cb656..9a21d6de0a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json +++ b/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 almak için buraya tıklayın.", + "GetConfirmationEmail": "Daha önce bir onay e-postası almadıysanız almak için buraya tıklayın.", "WhichLicenseTypeYouAreInterestedIn": "Hangi lisans türüyle ilgileniyorsunuz?", "BlackFridayDiscount": "Kara Cuma İndirimi" } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json index bce1fc11d7..be657892f7 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -735,7 +735,7 @@ "WatchTakeCloserLookVideo": "观看“详细了解ABP Suite 的代码生成”视频!", "ConfirmedEmailAddressRequiredToStartTrial": "你应该有一个确认的电子邮件地址,以便开始试用许可证。", "EmailVerificationMailNotSent": "电子邮件验证邮件不能发送。", - "GetConfirmationEmail": "点击这里获取确认邮件 如果你还没有收到。", + "GetConfirmationEmail": "点击这里获取确认邮件 如果你还没有收到。", "WhichLicenseTypeYouAreInterestedIn": "你感兴趣的许可证类型是什么?", "DontTakeOurWordForIt": "不要相信我们的话......", "ReadAbpCommercialUsersWantYouToKnow": "阅读 ABP Commercial 用户希望您了解到的内容", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 8cd1dc5a58..8cfb154295 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -13,7 +13,6 @@ "Status": "Status", "ContentSource": "Content Source", "Details": "Details", - "Url": "Url", "Title": "Title", "CreationTime": "Creation time", "Save": "Save", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index d693b60ac6..35698cd39c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -409,25 +409,13 @@ "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_": "Expert", - "Partner": "Partner", "Partner_": "Partnership", "WebSite": "Web Site", - "Industry": "Industry", - "Location": "Location", - "Contact": "Contact", - "ConsultantType": "Consultancy Type", "Expert_Year": "Expertise Year", - "Partner_Year": "Partnership Year", - "SpokenLanguages": "Spoken Languages", - "SocialMedia": "Social Media", "CompanyInfo": "Company Info", - "WhoWeAre": "Who We Are", - "RecentActivities": "Recent Activities", "Date": "Date", - "Activity": "Activity", - "Type": "Type", - "Contribution": "Contribution" + "WhoWeAre_Partner": "Who We Are", + "WhoWeAre_Expert": "About Me" } } diff --git a/cSpell.json b/cSpell.json deleted file mode 100644 index dd74bfb264..0000000000 --- a/cSpell.json +++ /dev/null @@ -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": "\\", - "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" - ] -} diff --git a/common.props b/common.props index faab44ccd2..262ed2cf6a 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 7.1.0 + 7.2.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/API/Application-Configuration.md b/docs/en/API/Application-Configuration.md index 8441459e02..5f2f2a9c6d 100644 --- a/docs/en/API/Application-Configuration.md +++ b/docs/en/API/Application-Configuration.md @@ -2,13 +2,14 @@ ABP Framework provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, the list of some fundamental information at this endpoint: -* [Localization](../Localization.md) values, supported and the current language of the application. -* Available and granted [policies](../Authorization.md) (permissions) for the current user. +* Granted [policies](../Authorization.md) (permissions) for the current user. * [Setting](../Settings.md) values for the current user. * Info about the [current user](../CurrentUser.md) (like id and user name). * Info about the current [tenant](../Multi-Tenancy.md) (like id and name). * [Time zone](../Timing.md) information for the current user and the [clock](../Timing.md) type of the application. +> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint. + ## HTTP API If you navigate to the `/api/abp/application-configuration` URL of an ABP Framework based web application or HTTP Service, you can access the configuration as a JSON object. This endpoint is useful to create the client of your application. @@ -19,5 +20,5 @@ For ASP.NET Core MVC (Razor Pages) applications, the same configuration values a See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI. -Other UI types provide services native to the related platform. For example, see the [Angular UI localization documentation](../UI/Angular/Localization.md) to learn how to use the localization values exposes by this endpoint. +Other UI types provide services native to the related platform. For example, see the [Angular UI settings documentation](../UI/Angular/Settings.md) to learn how to use the setting values exposes by this endpoint. diff --git a/docs/en/API/Application-Localization.md b/docs/en/API/Application-Localization.md new file mode 100644 index 0000000000..adf26c8fa8 --- /dev/null +++ b/docs/en/API/Application-Localization.md @@ -0,0 +1,33 @@ +# Application Localization Endpoint + +ABP Framework provides a pre-built and standard endpoint that returns all the [localization](../Localization.md) resources and texts defined in the server. + +> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint. + +## HTTP API + +`/api/abp/application-localization` is the main URL of the HTTP API that returns the localization data as a JSON string. I accepts the following query string parameters: + +* `cultureName` (required): A culture code to get the localization data, like `en` or `en-US`. +* `onlyDynamics` (optional, default: `false`): Can be set to `true` to only get the dynamically defined localization resources and texts. If your client-side application shares the same localization resources with the server (like ABP's Blazor and MVC UIs), you can set `onlyDynamics` to `true`. + +**Example request:** + +```` +/api/abp/application-localization?cultureName=en +```` + +## Script + +For [ASP.NET Core MVC (Razor Pages)](../UI/AspNetCore/Overall.md) applications, the same localization data is also available on the JavaScript side. `/Abp/ApplicationLocalizationScript` is the URL of the script that is auto-generated based on the HTTP API above. + +**Example request:** + +```` +/Abp/ApplicationLocalizationScript?cultureName=en +```` + +See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI. + +Other UI types provide services native to the related platform. For example, see the [Angular UI localization documentation](../UI/Angular/Localization.md) to learn how to use the localization values exposes by this endpoint. + diff --git a/docs/en/Application-Startup.md b/docs/en/Application-Startup.md new file mode 100644 index 0000000000..2839fcb19c --- /dev/null +++ b/docs/en/Application-Startup.md @@ -0,0 +1,318 @@ +## ABP Application Startup + +You typically use the [ABP CLI](CLI.md)'s `abp new` command to [get started](Getting-Started.md) with one of the pre-built [startup solution templates](Startup-Templates/Index.md). When you do that, you generally don't need to know the details of how the ABP Framework is integrated with your application or how it is configured and initialized. The startup template also comes with the fundamental ABP packages and [application modules](Modules/Index) are pre-installed and configured for you. + +> It is always suggested to [get started with a startup template](Getting-Started.md) and modify it for your requirements. Read this document only if you want to understand the details or if you need to modify how the ABP Framework starts. + +While the ABP Framework has a lot of features and integrations, it is built as a lightweight and modular framework. It consists of [hundreds of NuGet and NPM packages](https://abp.io/packages), so you can only use the features you need. If you follow the [Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application](Getting-Started-AspNetCore-Application.md) document, you'll see how easy it is to install the ABP Framework into an empty ASP.NET Core project from scratch. You only need to install a single NuGet package and make a few small changes. + +This document is for who wants to better understand how the ABP Framework is initialized and configured on startup. + +## Installing to a Console Application + +A .NET Console application is the minimalist .NET application. So, it is best to show the installing of the ABP Framework to a console application as a minimalist example. + +If you [create a new console application with Visual Studio](https://learn.microsoft.com/en-us/dotnet/core/tutorials/with-visual-studio) (for .NET 7.0 or later), you will see the following solution structure (I named the solution as `MyConsoleDemo`): + +![app-startup-console-initial](images/app-startup-console-initial.png) + +This example uses the [top level statements](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/top-level-statements), so it consists of only a single line of code. + +The first step is to install the [Volo.Abp.Core](https://www.nuget.org/packages/Volo.Abp.Core) NuGet package, which is the most core NuGet package of the ABP framework. You can install it using the [Package Manager Console](https://learn.microsoft.com/en-us/nuget/consume-packages/install-use-packages-powershell) in Visual Studio: + +````bash +Install-Package Volo.Abp.Core +```` + +Alternatively, you can use a command-line terminal in the root folder of the project (the folder containing the `MyConsoleDemo.csproj` file, for this example): + +````bash +dotnet add package Volo.Abp.Core +```` + +After adding the NuGet package, we should create a root [module class](Module-Development-Basics.md) for our application. We can create the following class in the project: + +````csharp +using Volo.Abp.Modularity; + +namespace MyConsoleDemo +{ + public class MyConsoleDemoModule : AbpModule + { + } +} +```` + +This is an empty class deriving from the `AbpModule` class. It is the main class that you will control your application's dependencies with, and implement your configuration and startup/shutdown logic. For more information, please check the [Modularity](Module-Development-Basics.md) document. + +As the second and the last step, change the `Program.cs` as shown in the following code block: + +````csharp +using MyConsoleDemo; +using Volo.Abp; + +// 1: Create the ABP application container +using var application = await AbpApplicationFactory.CreateAsync(); + +// 2: Initialize/start the ABP Framework (and all the modules) +await application.InitializeAsync(); + +Console.WriteLine("ABP Framework has been started..."); + +// 3: Stop the ABP Framework (and all the modules) +await application.ShutdownAsync(); +```` + +That's all. Now, ABP Framework is installed, integrated, started and stopped in your application. From now, you can install [ABP packages](https://abp.io/packages) to your application whenever you need them. + +## Installing a Framework Package + +If you want to send emails from your .NET application, you can use .NET's standard [SmtpClient class](https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient). ABP also provides an `IEmailSender` service that simplifies [sending emails](Emailing.md) and configuring the email settings in a central place. If you want to use it, you should install the [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) NuGet package to your project: + +````bash +dotnet add package Volo.Abp.Emailing +```` + +Once you add a new ABP package/module, you also need to specify the module dependency from your module class. So, change the `MyConsoleDemoModule` class as shown below: + +````csharp +using Volo.Abp.Emailing; +using Volo.Abp.Modularity; + +namespace MyConsoleDemo +{ + [DependsOn(typeof(AbpEmailingModule))] // Added the module dependency + public class MyConsoleDemoModule : AbpModule + { + } +} +```` + +I've just added a `[DependsOn]` attribute to declare that I want to use the ABP Emailing Module (`AbpEmailingModule`). Now, I can use the `IEmailSender` service in my `Program.cs`: + +````csharp +using Microsoft.Extensions.DependencyInjection; +using MyConsoleDemo; +using Volo.Abp; +using Volo.Abp.Emailing; + +using var application = await AbpApplicationFactory.CreateAsync(); +await application.InitializeAsync(); + +// Sending emails using the IEmailSender service +var emailsender = application.ServiceProvider.GetRequiredService(); +await emailsender.SendAsync( + to: "info@acme.com", + subject: "Hello World", + body: "My message body..." +); + +await application.ShutdownAsync(); +```` + +> If you run that application, you get a runtime error indicating that the email sending settings haven't been done yet. You can check the [Email Sending document](Emailing.md) to learn how to configure it. + +That's all. Install an ABP NuGet package, add the module dependency (using the `[DependsOn]` attribute) and use any service inside the NuGet package. + +The [ABP CLI](CLI.md) already has a special command to perform the addition of an ABP NuGet and also adding the `[DependsOn]` attribute to your module class for you with a single command: + +````bash +abp add-package Volo.Abp.Emailing +```` + +We suggest you to use the `abp add-package` command instead of manually doing it. + +## AbpApplicationFactory + +`AbpApplicationFactory` is the main class that creates an ABP application container. It provides a single static `CreateAsync` (and `Create` if you can't use asynchronous programming) method with multiple overloads. Let's investigate these overloads to understand where you can use them. + +The first overload gets a generic module class parameter as we've used before in this document: + +````csharp +AbpApplicationFactory.CreateAsync(); +```` + +The generic class parameter should be the root module class of your application. All the other modules are resolved as dependencies of that module. + +The second overload gets the module class as a `Type` parameter, instead of the generic parameter. So, the previous code block could be re-written as shown below: + +````csharp +AbpApplicationFactory.CreateAsync(typeof(MyConsoleDemoModule)); +```` + +Both overloads work exactly the same. So, you can use the second one if you don't know the module class type on development time and you (somehow) calculate it on runtime. + +If you create one of the methods above, ABP creates an internal service collection (`IServiceCollection`) and an internal service provider (`IServiceProvider`) to setup the [dependency injection](Dependency-Injection.md) system internally. Notice that we've used the `application.ServiceProvider` property in the *Installing a Framework Package* section to resolve the `IEmailSender` service from the dependency injection system. + +The next overload gets an `IServiceCollection` parameter from you to allow you to setup the dependency injection system yourself, or integrate to another framework (like ASP.NET Core) that also sets up the dependency injection system internally. + +We can change the `Program.cs` as shown below to externally manage the dependency injection setup: + +````csharp +using Microsoft.Extensions.DependencyInjection; +using MyConsoleDemo; +using Volo.Abp; + +// 1: Manually created the IServiceCollection +IServiceCollection services = new ServiceCollection(); + +// 2: Pass the IServiceCollection externally to the ABP Framework +using var application = await AbpApplicationFactory + .CreateAsync(services); + +// 3: Manually built the IServiceProvider object +IServiceProvider serviceProvider = services.BuildServiceProvider(); + +// 4: Pass the IServiceProvider externally to the ABP Framework +await application.InitializeAsync(serviceProvider); + +Console.WriteLine("ABP Framework has been started..."); + +await application.ShutdownAsync(); +```` + +In this example, we've used .NET's standard dependency injection container. The `services.BuildServiceProvider()` call creates the standard container. However, ABP provides an alternative extension method, `BuildServiceProviderFromFactory()`, that properly works even if you are using another dependency injection container: + +````csharp +IServiceProvider serviceProvider = services.BuildServiceProviderFromFactory(); +```` + +> You can check the [Autofac Integration](Autofac-Integration.md) document if you want to learn how you can integrate the [Autofac](https://autofac.org/) dependency injection container with the ABP Framework. + +Finally, the `CreateAsync` method has a last overload that takes the module class name as a `Type` parameter and a `IServiceCollection` object. So, we could re-write the last `CreateAsync` method usage as in the following code block: + +````csharp +using var application = await AbpApplicationFactory + .CreateAsync(typeof(MyConsoleDemoModule), services); +```` + +> All of the `CreateAsync` method overloads have `Create` counterparts. If your application type can not utilize asynchronous programming (that means you can't use the `await` keyword), then you can use the `Create` method instead of the `CreateAsync` method. + +### AbpApplicationCreationOptions + +All of the `CreateAsync` overloads can get an optional `Action` parameter to configure the options that are used on the application creation. See the following example: + +````csharp +using var application = await AbpApplicationFactory + .CreateAsync(options => + { + options.ApplicationName = "MyApp"; + }); +```` + +We've passed a lambda method to configure the `ApplicationName` option. Here's a list of all standard options: + +* `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. + +#### The ApplicationName option + +As defined above, the `ApplicationName` option is a human-readable name for the application. It is a unique value for an application. + +`ApplicationName` is used by the ABP Framework in several places to distinguish the application. For example, the [audit logging](Audit-Logging.md) system saves the `ApplicationName` in each audit log record written by the related application, so you can understand which application has created the audit log entry. So, if your system consists of multiple applications (like a microservice solution) that are saving audit logs to a single point, you should be sure that each application has a different `ApplicationName`. + +The `ApplicationName` property's value is set automatically from the **entry assembly's name** (generally, the project name in a .NET solution) by default, which is proper for most cases, since each application typically has a unique entry assembly name. + +There are two ways to set the application name to a different value. In this first approach, you can set the `ApplicationName` property in your application's [configuration](Configuration.md). The easiest way is to add an `ApplicationName` field to your `appsettings.json` file: + +````json +{ + "ApplicationName": "Services.Ordering" +} +```` + +Alternatively, you can set `AbpApplicationCreationOptions.ApplicationName` while creating the ABP application. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `ApplicationName` option as shown below: + +````csharp +await builder.AddApplicationAsync(options => +{ + options.ApplicationName = "Services.Ordering"; +}); +```` + +#### IApplicationInfoAccessor + +If you need to access the `ApplicationName` later in your solution, you can inject the `IApplicationInfoAccessor` service and get the value from its `ApplicationName` property. + +`IApplicationInfoAccessor` also provides an `InstanceId` value, that is a random GUID value that is generated when your application starts. You can use that value to distinguish application instances from each other. + +## IAbpApplication + +`AbpApplicationFactory` returns an `IAbpApplication` object from its `CreateAsync` (or `Create`) method. `IAbpApplication` is the main container for an ABP application. It is also registered to the [dependency injection](Dependency-Injection.md) system, so you can inject `IAbpApplication` in your services to use its properties and methods. + +Here's a list of `IAbpApplication` properties you may want to know: + +* `StartupModuleType`: Gets the root module of the application that was used while creating the application container (on the `AbpApplicationFactory.CreateAsync` method). +* `Services`: A list of all service registrations (the `IServiceCollection` object). You can not add new services to this collection after application initialization (you can actually add, but it won't have any effect). +* `ServiceProvider`: A reference to the root service provider used by the application. This can not be used before initializing the application. If you need to resolve non-singleton services from that `IServiceProvider` object, always create a new service scope and dispose it after usage. Otherwise, your application will have memory leak problems. See the *Releasing/Disposing Services* section of the [dependency injection](Dependency-Injection.md) document for more information about service scopes. +* `Modules`: A read-only list of all the modules loaded into the current application. Alternatively, you can inject the `IModuleContainer` service if you need to access the module list in your application code. + +The `IAbpApplication` interface extends the `IApplicationInfoAccessor` interface, so you can get the `ApplicationName` and `InstanceId` values from it. However, if you only need to access these properties, inject and use the `IApplicationInfoAccessor` service instead. + +`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(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. + +The [Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application](Getting-Started-AspNetCore-Application.md) document clearly explains how you can create an ABP application container in an ASP.NET Core application. + +You can also [create a console application](Startup-Templates/Console) to see how it is integrated with .NET Generic Host. + +> Most of the times, you will directly create ABP applications using the ABP CLI's `new` command. So, you don't need to care about these integration details. + +## See Also + +* [Dependency injection](Dependency-Injection.md) +* [Modularity](Module-Development-Basics.md) diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md index b3bc4335e1..2fdf34dca6 100644 --- a/docs/en/Audit-Logging.md +++ b/docs/en/Audit-Logging.md @@ -40,12 +40,14 @@ Here, a list of the options you can configure: * `HideErrors` (default: `true`): Audit log system hides and write regular [logs](Logging.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors. * `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users. * `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging). +* `IsEnabledForIntegrationService` (default: `false`): Audit Logging is disabled for [integration services](Integration-Services.md) by default. Set this property as `true` to enable it. * `IsEnabledForGetRequests` (default: `false`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET requests. * `DisableLogActionInfo` (default: `false`):If you set to true, Will no longer log `AuditLogActionInfo`. -* `ApplicationName`: If multiple applications saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. +* `ApplicationName`: If multiple applications are saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. If you don't set, it will set from the `IApplicationInfoAccessor.ApplicationName` value, which is the entry assembly name by default. * `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters. * `EntityHistorySelectors`: A list of selectors those are used to determine if an entity type is selected for saving the entity change. See the section below for details. * `Contributors`: A list of `AuditLogContributor` implementations. A contributor is a way of extending the audit log system. See the "Audit Log Contributors" section below. +* `AlwaysLogSelectors`: A list of selectors to save the audit logs for the matched criteria. ### Entity History Selectors diff --git a/docs/en/Background-Workers-Hangfire.md b/docs/en/Background-Workers-Hangfire.md index b076cf0395..5a456b1092 100644 --- a/docs/en/Background-Workers-Hangfire.md +++ b/docs/en/Background-Workers-Hangfire.md @@ -53,7 +53,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase CronExpression = Cron.Daily(); } - public override Task DoWorkAsync() + public override Task DoWorkAsync(CancellationToken cancellationToken = default) { Logger.LogInformation("Executed MyLogWorker..!"); return Task.CompletedTask; @@ -68,14 +68,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase ### UnitOfWork -For use with `UnitOfWorkAttribute`, you need to define an interface for worker: - ```csharp -public interface IMyLogWorker : IHangfireBackgroundWorker -{ -} - -[ExposeServices(typeof(IMyLogWorker))] public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker { public MyLogWorker() @@ -84,11 +77,13 @@ public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker CronExpression = Cron.Daily(); } - [UnitOfWork] - public override Task DoWorkAsync() + public override Task DoWorkAsync(CancellationToken cancellationToken = default) { - Logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; + using (var uow = LazyServiceProvider.LazyGetRequiredService().Begin()) + { + Logger.LogInformation("Executed MyLogWorker..!"); + return Task.CompletedTask; + } } } ``` @@ -105,9 +100,6 @@ public class MyModule : AbpModule ApplicationInitializationContext context) { await context.AddBackgroundWorkerAsync(); - - //If the interface is defined - //await context.AddBackgroundWorkerAsync(); } } ```` diff --git a/docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md b/docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md new file mode 100644 index 0000000000..85350d5efe --- /dev/null +++ b/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. diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/POST.md b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/POST.md new file mode 100644 index 0000000000..71494f09a6 --- /dev/null +++ b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/POST.md @@ -0,0 +1,207 @@ +# ABP.IO Platform 7.1 RC Has Been Released + +Today, we are happy to release the [ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) version **7.1 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v7.1! Thanks to all of you. + +## Get Started with the 7.1 RC + +Follow the steps below to try version 7.1.0 RC today: + +1) **Upgrade** the ABP CLI to version `7.1.0-rc.1` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 7.1.0-rc.1 +```` + +**or install** it if you haven't before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 7.1.0-rc.1 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. + +> You can also use the [Get Started](https://abp.io/get-started) page to generate a CLI command to create a new application. + +You can use any IDE that supports .NET 7.x, like [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). + +## Migrating to 7.1 + +This version doesn't introduce any breaking changes. However, Entity Framework developers may need to add a new code-first database migration to their projects since we made some improvements to the existing entities of some application modules. + +## What's New with ABP Framework 7.1? + +In this section, I will introduce some major features released in this version. In addition to these features, so many enhancements have been made in this version too. + +Here is a brief list of the titles explained in the next sections: + +* Blazor WASM option added to Application Single Layer Startup Template +* Introducing the `IHasEntityVersion` interface and `EntitySynchronizer` base class +* Introducing the `DeleteDirectAsync` method for the `IRepository` interface +* Introducing the `IAbpHostEnvironment` interface +* Improvements on the eShopOnAbp project +* Others + +### Blazor WASM option added to Application Single Layer Startup Template + +We've created the [Application (Single Layer) Startup Template](https://docs.abp.io/en/abp/7.1/Startup-Templates/Application-Single-Layer) in v5.2 with three UI types: Angular, Blazor Server and MVC. At the moment, we didn't provide UI option for Blazor, because it required 3 projects at least (server-side, client-side and shared library between these two projects). + +In this version, we've added the Blazor WASM option to the **Application (Single Layer) Startup Template**. It still contains three projects (`blazor`, `host`, and `contracts`) but hosted by a single `host` project. + +You can use the following CLI command to create an `app-nolayers` template with the Blazor UI as the UI option: + +```bash +abp new TodoApp -t app-nolayers -u blazor --version 7.1.0-rc.1 +``` + +> You can check the [Quick Start documentation](https://docs.abp.io/en/abp/7.1/Tutorials/Todo/Single-Layer/Index?UI=Blazor&DB=EF) for a quick start with this template. + +### Introducing the `IHasEntityVersion` interface and `EntitySynchronizer` base class + +Entity synchronization is an important concept, especially in distributed applications and module development. If we have an entity that is related to other modules, we need to align/sync their data once the entity changes and versioning entity changes can also be good, so we can know whether they're synced or not. + +In this version, [@gdlcf88](https://github.com/gdlcf88) made a great contribution to the ABP Framework and introduced the `IHasEntityVersion` interface which adds **auto-versioning** to entity classes and `EntitySynchronizer` base class to **automatically sync an entity's properties from a source entity**. + +You can check the issue and documentation from the following links for more info: + +- [Issue: Entity synchronizers and a new EntityVersion audit property](https://github.com/abpframework/abp/issues/14196) +- [Versioning Entities](https://docs.abp.io/en/abp/7.1/Entities#versioning-entities) +- [Distributed Event Bus - Entity Synchronizer](https://docs.abp.io/en/abp/7.1/Distributed-Event-Bus#entity-synchronizer) + +> Note: The entities of some modules from the ABP Framework have implemented the `IHasEntityVersion` interface. Therefore, if you are upgrading your application from an earlier version, you need to create a new migration and apply it to your database. + +### Introducing the `DeleteDirectAsync` method for the `IRepository` interface + +EF 7 introduced a new [`ExecuteDeleteAsync`](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) method that deletes entities without involving the change tracker into the process. Therefore, it's much faster. + +We've added the `DeleteDirectAsync` method to the `IRepository<>` interface to take the full power of EF 7. It deletes all entities that fit the given predicate. It directly deletes entities from the database, without fetching them. Therefore, some features (like **soft-delete**, **multi-tenancy**, and **audit logging)** won't work, so use this method carefully when you need it. And use the `DeleteAsync` method if you need those features. + +### Introducing the `IAbpHostEnvironment` interface + +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. + +**Usage:** + +```csharp +public class MyService +{ + private readonly IAbpHostEnvironment _abpHostEnvironment; + + public MyService(IAbpHostEnvironment abpHostEnvironment) + { + _abpHostEnvironment = abpHostEnvironment; + } + + public void MyMethod() + { + //getting the current environment name + var environmentName = _abpHostEnvironment.EnvironmentName; + + //check for the current environment + if (_abpHostEnvironment.IsDevelopment()) { /* ... */ } + } +} +``` + +You can inject the `IAbpHostEnvironment` into your service and get the current environment by using its `EnvironmentName` property. You can also check the current environment by using its extension methods such as `IsDevelopment()`. + +> Check the [ABP Application Startup](https://docs.abp.io/en/abp/7.1/Application-Startup) documentation for more information. + +### Improvements on the eShopOnAbp project + +K8s and Docker configurations have been made within this version (Dockerfiles and helm-charts have been added and image build scripts have been updated). See [#14083](https://github.com/abpframework/abp/issues/14083) for more information. + +### Others + +* Referral Links have been added to the CMS Kit Comment Feature (optional). You can specify common referral links (such as "nofollow" and "noreferrer") for links in the comments. See [#15458](https://github.com/abpframework/abp/issues/15458) for more information. +* ReCaptcha verification has been added to the CMS Kit Comment Feature (optional). You can enable ReCaptcha support to enable protection against bots. See the [documentation](https://docs.abp.io/en/abp/7.1/Modules/Cms-Kit/Comments) for more information. +* In the development environment, it is a must to reduce cache durations for some points. We typically don't have to invalidate the cache manually or wait on it for a certain time to be invalidated. For that purpose, we have reduced the cache durations for some points on the development environment. See [#14842](https://github.com/abpframework/abp/pull/14842) for more information. + +## What's New with ABP Commercial 7.1? + +We've also worked on [ABP Commercial](https://commercial.abp.io/) to align the new features and changes made in the ABP Framework. The following sections introduce a few new features coming with ABP Commercial 7.1. + +### Blazor WASM option added to Application Single Layer Pro Startup Template + +The [**Application (Single Layer) Startup Template**](https://docs.abp.io/en/commercial/latest/startup-templates/application-single-layer/index) with Blazor UI is also available for ABP Commercial customers with this version as explained above. + +You can use the following CLI command to create an `app-nolayers-pro` template with Blazor UI as the UI option: + +```bash +abp new TodoApp -t app-nolayers-pro -u blazor --version 7.1.0-rc.1 +``` + +You can also create an `app-nolayers-pro` template with Blazor UI via ABP Suite: + +![](suite-blazor-wasm-nolayers.png) + +### Suite - MAUI Blazor Code Generation + +We provided a new UI option "MAUI Blazor" for the `app-pro` template in the previous version and it's possible to create a `maui-blazor` application with both ABP CLI and ABP Suite. + +You can create an `app-pro` template with the MAUI Blazor as the UI option with the following ABP CLI command: + +```bash +abp new Acme.BookStore -t app-pro -u maui-blazor +``` + +In this version, we implemented the code generation for MAUI Blazor. You can create and generate CRUD pages for this new UI option as you do in other UI types. + +> Note: MAUI Blazor is currently only available with the `app-pro` template. + +### SaaS Module - Allowing entering a username while impersonating the tenant + +In the previous versions, we were able to impersonate a tenant from the [SaaS Module's Tenant Management UI](https://docs.abp.io/en/commercial/7.1/modules/saas#tenant-management). There was a constraint in this approach, which forced us to only impersonate the "admin" user. However, the tenant might change the admin user's username, or we may want to impersonate another user of the tenant. + +Thus, with this version, we decided to allow the impersonation of the tenant by the specified username. + +*You can click on the "Login with this tenant" action button:* + +![](saas-impersonation-1.png) + +*Then, Specify the admin name of the tenant:* + +![](saas-impersonation-2.png) + +## Community News + +### New ABP Community Posts + +* [Sergei Gorlovetsky](https://community.abp.io/members/Sergei.Gorlovetsky) has created two new community articles: + * [Why ABP Framework is one of the best tools for migration from legacy MS Access systems to latest Web app](https://community.abp.io/posts/why-abp-framework-is-one-of-the-best-tools-for-migration-from-legacy-ms-access-systems-to-latest-web-app-7l39eof0) + * [ABP Framework — 5 steps Go No Go Decision Tree](https://community.abp.io/posts/abp-framework-5-steps-go-no-go-decision-tree-2sy6r2st) +* [Onur Pıçakcı](https://github.com/onurpicakci) has created his first ABP community article that explains how to contribute to ABP Framework. You can read it 👉 [here](https://community.abp.io/posts/how-to-contribute-to-abp-framework-46dvzzvj). +* [Maliming](https://github.com/maliming) has created a new community article to show how to convert create/edit modals to a page. You can read it 👉 [here](https://community.abp.io/posts/converting-createedit-modal-to-page-4ps5v60m). + +We thank you all. We thank all the authors for contributing to the [ABP Community platform](https://community.abp.io/). + +### Volosoft Attended NDC London 2023 + +![](ndc-london.png) + +Core team members of the ABP Framework, [Halil Ibrahim Kalkan](https://twitter.com/hibrahimkalkan) and [Alper Ebicoglu](https://twitter.com/alperebicoglu) attended [NDC London 2023](https://ndclondon.com/) from the 23rd to the 27th of January. + +### Community Talks 2023.1: LeptonX Customization + +![](community-talks-conver-image.png) + +In this episode of ABP Community Talks, 2023.1; we'll talk about **LeptonX Customization**. We will dive into the details and show you how to customize the [LeptonX Theme](https://leptontheme.com/) with examples. + +The event will be live on Thursday, February 16, 2023 (20:00 - 21:00 UTC). + +> Register to listen and ask your questions now 👉 https://kommunity.com/volosoft/events/abp-community-talks-20231-leptonx-customization-03f9fd8c. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://docs.abp.io/en/abp/7.1/Road-Map) documentation to learn about the release schedule and planned features for the next releases. Please try the ABP v7.1 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/community-talks-conver-image.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/community-talks-conver-image.png new file mode 100644 index 0000000000..6d7ccb5b4c Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/community-talks-conver-image.png differ diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/cover-image.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/cover-image.png new file mode 100644 index 0000000000..dd9e0475cb Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/ndc-london.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/ndc-london.png new file mode 100644 index 0000000000..cddf3d01b8 Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/ndc-london.png differ diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-1.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-1.png new file mode 100644 index 0000000000..520cb6857a Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-1.png differ diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-2.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-2.png new file mode 100644 index 0000000000..7b5c685f35 Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/saas-impersonation-2.png differ diff --git a/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/suite-blazor-wasm-nolayers.png b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/suite-blazor-wasm-nolayers.png new file mode 100644 index 0000000000..0f43c3cd2c Binary files /dev/null and b/docs/en/Blog-Posts/2023-02-08 v7_1_Preview/suite-blazor-wasm-nolayers.png differ diff --git a/docs/en/CLI.md b/docs/en/CLI.md index c24dedae62..9feb810730 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -138,6 +138,7 @@ For more samples, go to [ABP CLI Create Solution Samples](CLI-New-Command-Sample * `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks: * `mvc`: ASP.NET Core MVC. * `angular`: Angular UI. + * `blazor`: Blazor UI. * `blazor-server`: Blazor Server UI. * `none`: Without UI. * `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers: @@ -353,6 +354,7 @@ abp generate-proxy -t csharp -url https://localhost:44302/ * `--type` or `-t`: The name of client type. Available clients: * `csharp`: C#, work in the `*.HttpApi.Client` project directory. There are some additional options for this client: + * `--without-contracts`: Avoid generating the application service interface, class, enum and dto types. * `--folder`: Folder name to place generated CSharp code in. Default value: `ClientProxies`. * `ng`: Angular. There are some additional options for this client: * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 8fafd725be..e39e0ead24 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -10,7 +10,7 @@ ABP Framework extends the [ASP.NET Core distributed cache](https://docs.microsof [Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching) is the main package of the caching system. You can install it a project using the add-package command of the [ABP CLI](CLI.md): -``` +```bash abp add-package Volo.Abp.Caching ``` @@ -252,6 +252,14 @@ ABP's distributed cache interfaces provide methods to perform batch methods thos > These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](Redis-Cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` ... methods (called once for each item). +## Caching Entities + +ABP Framework provides a [Distributed Entity Cache System](Entity-Cache.md) for caching entities. It is useful if you want to use caching for quicker access to the entity rather than repeatedly querying it from the database. + +It's designed as read-only and automatically invalidates a cached entity if the entity is updated or deleted. + +> See the [Entity Cache](Entity-Cache.md) documentation for more information. + ## Advanced Topics ### Unit Of Work Level Cache @@ -272,4 +280,5 @@ You can [replace](Dependency-Injection.md) this service by your own implementati ## See Also -* [Redis Cache](Redis-Cache.md) \ No newline at end of file +* [Entity Cache](Entity-Cache.md) +* [Redis Cache](Redis-Cache.md) diff --git a/docs/en/Community-Articles/2022-11-25-JSON-columns/post.md b/docs/en/Community-Articles/2022-11-25-JSON-columns/post.md index cb8ceafc88..4f69b4eb1f 100644 --- a/docs/en/Community-Articles/2022-11-25-JSON-columns/post.md +++ b/docs/en/Community-Articles/2022-11-25-JSON-columns/post.md @@ -68,7 +68,7 @@ public class MyDbContext : AbpDbContext builder.Entity(b => { - b.ToTable(MyProjectConsts.DbTablePrefix + "Persons", MyProjecConsts.DbSchema); + b.ToTable(MyProjectConsts.DbTablePrefix + "Persons", MyProjectConsts.DbSchema); b.ConfigureByConvention(); b.OwnsOne(x=>x.ContactDetails, c => { @@ -125,6 +125,9 @@ As you can see, thanks to JSON Columns feature the **ContactDetails** row has JS In this article, I've briefly introduced the JSON Columns feature that was shipped with EF Core 7. It's pretty straightforward to use JSON Columns in an ABP based application. You can see the examples above and give it a try! +### The Source Code +* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreJSONColumnDemo). + ### References * [https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns) diff --git a/docs/en/Community-Articles/2022-11-28-ef-bulk-operations/POST.md b/docs/en/Community-Articles/2022-11-28-ef-bulk-operations/POST.md new file mode 100644 index 0000000000..ac517506ec --- /dev/null +++ b/docs/en/Community-Articles/2022-11-28-ef-bulk-operations/POST.md @@ -0,0 +1,71 @@ +# Bulk Operations with Entity Framework Core 7.0 +Entity Framework tracks all the entity changes and applies those changes to the database one by one when the `SaveChanges()` method is called. There was no way to execute bulk operations in Entity Framework Core without a dependency. + +As you know the [Entity Framework Extensions](https://entityframework-extensions.net/bulk-savechanges) library was doing it but it was not free. + +There was no other solution until now. [Bulk Operations](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) are now available in Entity Framework Core with .NET 7. + +With .NET 7, there are two new methods such as `ExecuteUpdate` and `ExecuteDelete` available to execute bulk operations. It's a similar usage with the Entity Framework Core Extensions library if you're familiar with it. + +You can visit the microsoft example [here](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) about how to use it. + + +It can be easily used with the DbContext. + +```csharp +await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync(); +``` + +## Using with ABP Framework +ABP Framework provides an abstraction over database operations and implements generic repository pattern. So, DbContext can't be accessed outside of [repositories](https://docs.abp.io/en/abp/latest/Repositories). + +You can use the `ExecuteUpdate` and `ExecuteDelete` methods inside a repository. + +```csharp +public class BookEntityFrameworkCoreRepository : EfCoreRepository, IBookRepository +{ + public BookEntityFrameworkCoreRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public async Task UpdateListingAsync() + { + var dbSet = await GetDbSetAsync(); + + await dbSet + .Where(x => x.IsListed && x.PublishedOn.Year <= 2022) + .ExecuteUpdateAsync(s => s.SetProperty(x => x.IsListed, x => false)); + } + + public async Task DeleteOldBooksAsync() + { + var dbSet = await GetDbSetAsync(); + + await dbSet + .Where(x => x.PublishedOn.Year <= 2000) + .ExecuteDeleteAsync(); + } +} +``` + +There is no need to take an action for bulk inserting. You can use the `InsertManyAsync` method of the repository instead of creating a new method for it if you don't have custom logic. It'll use a new bulk inserting feature automatically since it's available in EF Core 7.0. + +```csharp +public class MyDomainService : DomainService +{ + protected IRepository BookRepository { get; } + + public MyDomainService(IRepository bookRepository) + { + BookRepository = bookRepository; + } + + public async Task CreateBooksAsync(List books) + { + // It'll use bulk inserting automatically. + await BookRepository.InsertManyAsync(books); + } +} +``` + +> If you use `ExecuteDeleteAsync` or `ExecuteUpdateAsync`, then ABP's soft delete and auditing features can not work. Because these ABP features work with EF Core's change tracking system and these new methods doesn't work with the change tracking system. So, use them carefully. diff --git a/docs/en/Community-Articles/2022-11-29-EF-Core-Guarded-Types/POST.md b/docs/en/Community-Articles/2022-11-29-EF-Core-Guarded-Types/POST.md new file mode 100644 index 0000000000..53903e1ccc --- /dev/null +++ b/docs/en/Community-Articles/2022-11-29-EF-Core-Guarded-Types/POST.md @@ -0,0 +1,208 @@ +# Value generation for DDD guarded types with Entity Framework Core 7.0 + +In domain-driven design (DDD), *guarded keys* can improve the type safety of key properties. This is achieved by wrapping the key type in another type which is specific to the use of the key. In this article, I will explain the cases why you may need to use guarded types and discuss the advantages and limitations when implementing to an ABP application. + +> You can find the source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo). + +## The Problem + +While developing an applications, there are many cases where we manually assign foreign keys that can be in guid type or integer type, etc. This manual assignment mistakes can cause miss-match of unique identifiers, such as **assigning a product ID to a category**, that can be hard to detect in the future. + +Here is a very simplified sample of wrong assignment when trying to update a product category: + +````csharp +public class ProductAppService : MyProductStoreAppService, IProductAppService +{ + private readonly IRepository _productRepository; + + public ProductAppService(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task UpdateProductCategoryAsync(Guid productId, Guid categoryId) + { + var productToUpdate = await _productRepository.GetAsync(productId); + productToUpdate.CategoryId = productId; // Wrong assignment that causes error only at run-time + + await _productRepository.UpdateAsync(productToUpdate); + } +} +```` + +While the sample demonstrates a very simple mistake, it is easier to come across similar mistakes when the business logic gets more complex especially when you are using methods with **multiple foreign key arguments**. The next section offers using guarded types to prevent these kind of problems as a solution to the problem. + +## The Solution + +Strongly-typed IDs (*guarded keys*) is a DDD approach to address this problem. One of the main problems with .NET users was handling the persisting these objects. With EFCore7, key properties can be guarded with type safety seamlessly. + +To use guarded keys, update your aggregate root or entity unique identifier with a complex type to overcome *primitive obsession*: + +````csharp +public readonly struct CategoryId +{ + public CategoryId(Guid value) => Value = value; + public Guid Value { get; } +} + +public readonly struct ProductId +{ + public ProductId(Guid value) => Value = value; + public Guid Value { get; } +} +```` + +You can now use these keys for your aggregate roots or entities: + +```csharp +public class Product : AggregateRoot +{ + public ProductId Id { get; set; } + public string Name { get; set; } + public CategoryId CategoryId { get; set; } + + private Product() { } + + public Product(ProductId id, string name) : base(id) + { + Name = Check.NotNullOrEmpty(name, nameof(name)); + } +} + +public class Category : AggregateRoot +{ + public CategoryId Id { get; set; } + public string Name { get; set; } + public List Products { get; } = new(); + + private Category() { } + + public Category(CategoryId id, string name) : base(id) + { + Name = Check.NotNullOrEmpty(name, nameof(name)); + } +} +``` + +`ProductId` and `CategoryId` guarded key types shown in the sample use `Guid` key values, which means Guid values will be used in the mapped database tables. This is achieved by defining [value converters](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions) for the types. + +Override the `ConfigureConventions` method of your DbContext to use the value converters: + +````csharp +protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) +{ + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); +} + +private class ProductIdConverter : ValueConverter +{ + public ProductIdConverter() + : base(v => v.Value, v => new(v)) + { + } +} + +private class CategoryIdConverter : ValueConverter +{ + public CategoryIdConverter() + : base(v => v.Value, v => new(v)) + { + } +} +```` + +> The code here uses `struct` types. This means they have appropriate value-type semantics for use as keys. If `class` types are used instead, then they need to either override equality semantics or also specify a [value comparer](https://learn.microsoft.com/en-us/ef/core/modeling/value-comparers). + +Now, you can use generic (or custom) repositories of ABP using the guarded type as the key for the repository: + +```csharp +public class ProductStoreDataSeedContributor : IDataSeedContributor, ITransientDependency +{ + private readonly IRepository _categoryRepository; + private readonly IRepository _productRepository; + + public ProductStoreDataSeedContributor( + IRepository categoryRepository, + IRepository productRepository + ) + { + _categoryRepository = categoryRepository; + _productRepository = productRepository; + } + + // ... +} +``` + +You can also use `integer` as guarded type for your key properties and use [Sequence-based key generation for SQL Server](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#sequence-based-key-generation-for-sql-server) for value generation. + +## Discussions + +In this section, I will discuss the use cases of guarded types and limitations when implementing to an ABP application. + +### Do I need to use guarded types? + +If you are already following the best practices of [Domain-Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design), you are aware that **updates** and **creations** of an aggregate are done **over** the aggregate root itself as a whole unit. And entity state changes of an aggregate root should be done using the [domain services](https://docs.abp.io/en/abp/latest/Domain-Services). Domain services should already validate the entity. + +**Example: Using domain service to update product:** + +````csharp +public class ProductManager : DomainService +{ + private readonly IRepository _productRepository; + + public ProductManager(IRepository productRepository) + { + _productRepository = productRepository; + } + + public Task AssignCategory(Product product, Category category) + { + // ... + + product.CategoryId = category.Id; + + //.. + } +} +```` + +In this sample, domain service validates that both **product** and the **category** entities, passed by the application layer, are valid objects since they are not key properties. However, manual assignment is already in place and more complex the domain logic, higher to miss out mistakes. At the end, it will depend on your tolerance level for developer errors comparing to the time you want to spend time on additional code base for guarded types. + +### Limitations + +One important limitation is automatic value generation when using `Guid` as guarded type for your key properties. The basic repository can not generate the unique identifier automatically by the time this article is written: + +```csharp +public readonly struct ProductId +{ + public ProductId(Guid value) => Value = value; + public Guid Value { get; } +} +``` + +you need to generate the unique identifier **manually**: + +````csharp +var newProduct = await _productRepository.InsertAsync( + new Product(new ProductId(_guidGenerator.Create()), "New product") +); +```` + +## Conclusion + +In this article, I tried to explain DDD guarded types for key properties and value generation for these properties using Entity Framework 7.0 and ABP. + +Using strongly-typed key properties reduce the chance of unnoticed errors. Admittedly it increases the code complexity by adding new types to your solution with extra coding, especially if you are using classes as keys. Guarded types provide improved safety for your code base at the expense of additional code complexity as in many DDD concepts and patterns. + +If you have a large team working on a large scale solution containing complex business logics where key assignments are abundant or if you are using methods with multiple foreign key arguments, I personally suggest using guarded types. + +## The Source Code + +* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo). + +## See Also + +* [What's new in EF Core 7.0](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew) +* [ABP Framework: Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-11-29-Rate-Limiting/Post.md b/docs/en/Community-Articles/2022-11-29-Rate-Limiting/Post.md new file mode 100644 index 0000000000..d26f6c4aaa --- /dev/null +++ b/docs/en/Community-Articles/2022-11-29-Rate-Limiting/Post.md @@ -0,0 +1,231 @@ +# Rate Limiting with ASP.NET Core 7.0 + +Rate limiting is a way of controlling the traffic that a web application or API receives. In other words, rate limiting helps you control the amount of traffic each user has access to at any given time. This is extremely useful when you want to manage the load on your server or services, avoid going over your monthly data transfer limit and allow the system to continue to function and meet service level agreements, even when an increase in demand places an extreme load on resources. + +In this article, we will look at what rate limiting is, why we need to use it, how the different rate limiting algorithms provided with .NET 7.0 work, and best practices for using rate limiting in your application. + +## What is rate limiting? + +Whether accidental or intentional, users may exhaust resources in a way that impacts others. When a number of requests are received on to resources for a long time, the server can run out of those resources. These resources can include memory, threads, connections, or anything else that is limited. To avoid this situation, set rate limiters. Rate limiters control the consumption of resources used by an instance of an application, a user, an individual tenant, or an entire service. + +## Why do you need to use rate limiting? + +A rate limiting system is crucial in any application where you have to control or throttle user requests or traffic. This is especially true in applications running on a cloud hosting platform because the user’s traffic can affect the whole server where the application is hosted. + +Why do you need to implement rate limiting? Here are a few reasons: + +- To ensure that a system continues to meet service level agreements (SLA). +- To prevent a single user, tenant, service, or so on from monopolizing the resources provided by an application. +- To help cost-optimize a system by limiting the maximum resource levels needed to keep it functioning. + +## Rate limiter algorithms + +The [`RateLimiterOptionsExtensions`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions) class provides the following extension methods for rate limiting: + +- **[Fixed window](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#fixed-window-limit)**: Fixed-window limits—such as 3,000 requests per hour or 10 requests per day—are easy to state, but they are subject to spikes at the edges of the window, as available quota resets. Consider, for example, a limit of 3,000 requests per hour, which still allows for a spike of all 3,000 requests to be made in the first minute of the hour, which might overwhelm the service. +- [**Sliding window**:](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#sliding-window-limit) Sliding windows have the benefits of a fixed window, but the rolling window of time smoothes out bursts. Systems such as Redis facilitate this technique with expiring keys. +- [**Token bucket**](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#token-bucket-limit): A token bucket maintains a rolling and accumulating budget of usage as a balance of tokens. A token bucket adds tokens at some rate. When a service request is made, the service attempts to withdraw a token (decrementing the token count) to fulfill the request. If there are no tokens in the bucket, the service has reached its limit and responds with backpressure. +- [**Concurrency**](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#concurrency-limiter): A concurrency limiter is the simplest form of rate limiting. It doesn’t look at time, just at number of concurrent requests. + +In order to be a more realistic example, instead of making an example with each rate limiter algorithm, we will implement the following three algorithms in an **ABP-based** application. + +1. We will add a `SlidingWindowLimiter` with a partition for all anonymous users. +2. We will add a `TokenBucketRateLimiter` with a partition for each authenticated user. +3. We will add a `ConcurrencyLimiter` with a partition for each Tenant. + +**Note:** The following sample isn't meant for production code but is an example of how to use the limiters in ABP-based applications. + +### Limiter with `OnRejected`, `RetryAfter`, and `GlobalLimiter` + +#### Add rate limiter + +Let's create the following method in the `MyProjectNameWebModule.cs` class in the `MyProjectName.Web` project. + +**Note:** If the `**.Web` project is not in your application, you can do the same in the project where your application is hosted. + +```csharp +private void ConfigureRateLimiters(ServiceConfigurationContext context) +{ + context.Services.AddRateLimiter(limiterOptions => + { + limiterOptions.OnRejected = (context, cancellationToken) => + { + if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) + { + context.HttpContext.Response.Headers.RetryAfter = + ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo); + } + + context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; + context.HttpContext.RequestServices.GetService()? + .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") + .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path); + + return new ValueTask(); + }; + + limiterOptions.AddPolicy("UserBasedRateLimiting", context => + { + var currentUser = context.RequestServices.GetService(); + + if (currentUser is not null && currentUser.IsAuthenticated) + { + return RateLimitPartition.GetTokenBucketLimiter(currentUser.UserName, _ => new TokenBucketRateLimiterOptions + { + TokenLimit = 10, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 3, + ReplenishmentPeriod = TimeSpan.FromMinutes(1), + TokensPerPeriod = 4, + AutoReplenishment = true + }); + } + + return RateLimitPartition.GetSlidingWindowLimiter("anonymous-user", + _ => new SlidingWindowRateLimiterOptions + { + PermitLimit = 2, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1, + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = 2 + }); + }); + + limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create(context => + { + var currentTenant = context.RequestServices.GetService(); + + if (currentTenant is not null && currentTenant.IsAvailable) + { + return RateLimitPartition.GetConcurrencyLimiter(currentTenant!.Name, _ => new ConcurrencyLimiterOptions + { + PermitLimit = 5, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 1 + }); + } + + return RateLimitPartition.GetNoLimiter("host"); + }); + }); +} +``` + +In the above example, the `TokenBucketLimiter` is used for each authenticated user, while the `SlidingWindowLimiter` is used for all anonymous users. Additionally, as a global limiter, the `ConcurrencyLimiter` is used for each tenant, while rate limiting is disabled for the host(tenant is not available). Also, for requests that are rejected when the limit is reached, sets the response status code to [429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) and the response mentions when to retry (if available from the rate-limiting metadata). + +Let's call the `ConfigureRateLimiters` method that we created in the `ConfigureServices` method. + +The final version of the `ConfigureServices` method: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureBundles(); + ConfigureUrls(configuration); + ConfigurePages(configuration); + ConfigureAuthentication(context); + ConfigureImpersonation(context, configuration); + ConfigureAutoMapper(); + ConfigureVirtualFileSystem(hostingEnvironment); + ConfigureNavigationServices(); + ConfigureAutoApiControllers(); + ConfigureSwaggerServices(context.Services); + ConfigureExternalProviders(context); + ConfigureHealthChecks(context); + ConfigureCookieConsent(context); + ConfigureTheme(); + + Configure(options => + { + options.IsDynamicPermissionStoreEnabled = true; + }); + + ConfigureRateLimiters(context); // added +} +``` + +#### Add RateLimiter middleware + +Add the following line just before the `app.UseConfiguredEndpoints(...)` line to add the `RateLimiter` middleware to your ASP.NET Core request pipeline: + +```csharp +app.UseRateLimiter(); +``` + +#### Use rate limiter for all controllers + +Let's edit the `ConfiguredEndpoints` middleware as follows: + +```csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapRazorPages() + .DisableRateLimiting(); + + endpoints.MapControllers() + .RequireRateLimiting("UserBasedRateLimiting"); +}); +``` + +- **DisableRateLimiting:** It is used to disable the `ConcurrencyLimiter` for razor pages, which we set globally when the tenant is available. +- **RequireRateLimiting:** We have enabled the rate limiter, which we define according to whether the user is authenticated or not, for all controllers. + +## `EnableRateLimiting` and `DisableRateLimiting` attributes + +It's kind of unrealistic to always use rate limiting for all controllers or pages. Sometimes, we may want to throttle a particular endpoint or page. In such cases, we can use the `EnableRateLimiting` and `DisableRateLimiting` attributes. The `EnableRateLimiting` and `DisableRateLimiting` attributes can be applied to a controller, action method, or razor rage. Check [here](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#enableratelimiting-and-disableratelimiting-attributes) for more. + +## Rate limit an HTTP handler + +Rate limiting when sending an HTTP request can be a good practice, especially in service-to-service communication. Because, resources are consumed by apps that rely on them, and when an app makes too many requests for a single resource, it can lead to *resource contention*. Resource contention occurs when a resource is consumed by too many clients, and the resource is unable to serve all of the apps that are requesting it. This can result in a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. Since there are similar codes, I will not mention an example in this article, but to avoid such situations, you can write your own HTTP handler as [here](https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter#implement-a-delegatinghandler-subclass). + +## How does it work? + +[System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting) provides the primitives for writing rate limiters as well as providing a few commonly used algorithms built-in. The main type is the abstract base class [RateLimiter](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs). + +```csharp +public abstract class RateLimiter : IAsyncDisposable, IDisposable +{ + public abstract int GetAvailablePermits(); + public abstract TimeSpan? IdleDuration { get; } + + public RateLimitLease Acquire(int permitCount = 1); + public ValueTask WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default); + + public void Dispose(); + public ValueTask DisposeAsync(); +} +``` + +`RateLimiter` contains `Acquire` and `WaitAsync` as the core methods for trying to gain permits for a resource that is being protected. Depending on the application, the protected resource may need to acquire more than 1 permits, so `Acquire` and `WaitAsync` both accept an optional `permitCount` parameter. `Acquire` is a synchronous method that will check if enough permits are available or not and return a `RateLimitLease` which contains information about whether you successfully acquired the permits or not. `WaitAsync` is similar to `Acquire` except that it can support queuing permit requests which can be de-queued at some point in the future when the permits become available, which is why it’s asynchronous and accepts an optional `CancellationToken` to allow canceling the queued request. + +`RateLimitLease` has an `IsAcquired` property which is used to see if the permits were acquired. Additionally, the `RateLimitLease` may contain metadata such as a suggested retry-after period if the lease failed. Finally, the `RateLimitLease` is disposable and should be disposed when the code is done using the protected resource. The disposal will let the `RateLimiter` know to update its limits based on how many permits were acquired. + +## Limitations + +In most cases, the rate-limiting middleware provided with ASP.NET 7.0 will meet your requirements. However, if you would want to return statistics about your limits (e.g. [the way GitHub does](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-http-headers)), you’ll find out that the ASP.NET rate limiting middleware does not support this. You won’t have access to the “number of requests remaining” or other metadata. Not in `OnRejected`, and definitely not if you want to return this data as headers on every request. + +## Best practices for rate limiting + +In order to use rate limiting properly, you need to have a solid understanding of the types of limiting available, as well as the data rate and data volume of your service. You also need to have a clear idea of how many users you expect to use your service as well as how they will interact with it. The best practices for rate limiting are as follows: +- Find the right rate limiter algorithm for your endpoint. I mean, the cost of an endpoint should be considered when selecting a limiter. The cost of an endpoint includes the resources used, for example, time, data access, CPU, and I/O. +- Set realistic limits. Once you’ve figured out all the above, you need to set realistic limits for each service. Then, before deploying an app using rate limiting to production, stress test the app to validate the rate limiters and options used. For example, create a [JMeter script](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) with a tool like [BlazeMeter](https://guide.blazemeter.com/hc/articles/207421695-Writing-your-first-JMeter-script) or [Apache JMeter HTTP(S) Test Script Recorder](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) and load the script to [Azure Load Testing](https://learn.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing). +- In response to rate-limiting, intermittent, or non-specific errors, a client should generally retry the request after a delay. It is a best practice for this delay to increase exponentially after each failed request, which is referred to as *exponential backoff*. When many clients might be making schedule-based requests (such as fetching results every hour), additional random time (*jitter*) should be applied to the request timing, the backoff period, or both of them to ensure that these multiple client instances don't become periodic [thundering herd](https://www.wikiwand.com/en/Thundering_herd_problem), and cause a form of DDoS themselves. + +## Conclusion + +In this article, we’ve covered what rate limiting is, why you need to use it and the best practices for doing so. We’ve also looked at how to use three rate-limiting algorithms that are provided with .NET 7.0 on ABP-based applications and how rate-limiting works. Now that you’re familiar with the concept of rate limiting, it’s time to start implementing rate limiting in your application. This will allow you to control the traffic and ensure that your application is running smoothly without any issues. + +## References + +- https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0 +- https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/ +- https://blog.maartenballiauw.be/post/2022/09/26/aspnet-core-rate-limiting-middleware.html +- https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter +- https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern +- https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling +- https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet +- https://cloud.google.com/architecture/rate-limiting-strategies-techniques diff --git a/docs/en/Community-Articles/2022-11-29-Rate-Limiting/cover-image.png b/docs/en/Community-Articles/2022-11-29-Rate-Limiting/cover-image.png new file mode 100644 index 0000000000..cedae39a3b Binary files /dev/null and b/docs/en/Community-Articles/2022-11-29-Rate-Limiting/cover-image.png differ diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md new file mode 100644 index 0000000000..fd98089505 --- /dev/null +++ b/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": "", + "ClientSecret": "" + } +```` + +## 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) \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg new file mode 100644 index 0000000000..cfb248ebc4 Binary files /dev/null and b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg differ diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg new file mode 100644 index 0000000000..8157a4e952 Binary files /dev/null and b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/how-can-I-contribute-to-open-source-projects-on-github.md b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/how-can-I-contribute-to-open-source-projects-on-github.md new file mode 100644 index 0000000000..50b980cf8d --- /dev/null +++ b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/how-can-I-contribute-to-open-source-projects-on-github.md @@ -0,0 +1,105 @@ +# How to Contribute to ABP Framework + +## Introduction + +In this article I will explain how you can contribute to the open source ABP Framework. You will not only learn about the ABP Framework, but also how to contribute to an open source project, what are the standard rules, some git operations, etc. + +## What is Open Source? + +Open source software is code designed to be publicly available. Anyone can view, use, modify and distribute the project and code. The fact that the code is open source makes it a natural community and open for improvement. This enables ideas and thoughts to spread rapidly. + +## What is ABP Framework? + +ABP Framework is a complete infrastructure for building modern web applications following the best practices and guidelines of software development. ABP Framework is completely free, [open source](https://github.com/abpframework) and community driven. ABP is a modular framework and Application Modules provide pre-built application functionalities. + +## Before Contribution + +Before making any changes and trying to push them to the target repository we need to create a new [issue](https://github.com/abpframework/abp/issues) if there are no issues with the work. If there is an existing issue, you can proceed through this issue. This way, no other developer will work on the same issue and your PR will have a better chance to be accepted. + +Previous ABP Community Talk on this topic can be found [here](https://www.youtube.com/watch?v=Wz4Z-O-YoPg). + +## GitHub Issues +You may want to fix a known bug or work on a planned enhancement. See the [issue list](https://github.com/abpframework/abp/issues) on GitHub. + +## Feature Requests +If you have a feature idea for the framework or modules, create an issue on GitHub or attend an existing discussion. Then you can implement it if it's embraced by the community. + +## How to Contribute to an Open Source Software? +There are some steps to contribute to OSS projects. You can follow the steps below. + +## Step 1: Fork the Project + +The first thing we need to do now is to fork the open source project. Forking will create a copy of the project in your own GitHub account. This will allow users to make changes to the code without affecting the original repository. Just press the fork key in the project. + +![fork-image](images/fork-project-image.png) + +After forking, it will create a new repo in your own GitHub profile. + +![fork-image-profile](images/fork-project-profile.png) + +## Step 2: Clone the Project + +In order to develop the project, you need to clone it to your local. After clicking on the code button, select your preferred cloning method and copy the link. You can run the copied link on your local machine with the `git clone` command, but we will use GitHub Desktop. Press `Open with GitHub Desktop` and the repo will be installed on your local machine. + +or alternatively use the `git clone https://github.com/username/abp.git` command + +![clone-image](images/clone-image.png) + +## Step 3: Create a New Branch + +In this step, you need to create a new branch of your own before you start developing it. Open the repo on GitHub Desktop and create a new branch. When creating a new branch, be careful which branch you create it on. + +or alternatively use the `git checkout -b new-branch` command + +![branch-image](images/branch-image.png) + +## Step 4: Development + +Choose a suitable IDE to develop on the new branch you created. In order not to complicate things, we will create a `Developers.md` file and process it. Let's enter a sample text in the Developers file. + +![developer-list](images/developer-list.png) + +As you can see, all changes made to the repo are reflected directly on GitHub Desktop. + +![github-desktop-change](images/github-desktop-change.png) + +## Step 5: Commit + +The commit operation is used to save the changes you have made. It is useful to commit after certain operations are done in the project. It is useful to write a short sentence describing what you've done for the changes made in each commit. Press the `Commit to ` button to commit. + +or alternatively use the `git add .` and `git commit -m "Added the Developer List"` command + +![commit-image](images/commit-image.png) + +## Step 6: Publish the Changes + +The changes you have made so far are only visible on your local machine. You need to publish these changes to submit them to your forked repository. Please press the publish branch button to publish. + +or alternatively use the `git push origin new-branch` command + + +![push-image](images/git-push-image.png) + +## Step 7: Create a Pull Request + +After the push, the pull request `Create Pull Request` button will appear on GitHub Desktop. Click it and create a pull request. + +![github-desktop-pull-request](images/github-desktop-pull-request.png) + +You can also make a pull request from the repo in your GitHub profile. + +![compare-pull-request](images/pull-request-image.png) + +Before creating the pull request, make sure that the branch you created is making changes to the correct branch. After briefly describing your changes in the title and description, click the `Create pull request` button. This will send a pull request to the original repository. If the pull request is approved and merged by the community, your changes will also appear in the main repository. + +![open-pull-request](images/open-pull-request-image.png) + +That's it! You have contributed your development to an open source project. + +## Conclusion +In this article, I showed you how you could contribute to the ABP Framework, an open source and community driven project. Thank you for reading the article, I hope it was useful. See you soon! + +## References +- https://opensource.guide/how-to-contribute/ +- https://docs.abp.io/en/abp/latest/Contribution/Index + diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/branch-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/branch-image.png new file mode 100644 index 0000000000..ed2e4f083f Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/branch-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/clone-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/clone-image.png new file mode 100644 index 0000000000..6e22d8fbd3 Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/clone-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/commit-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/commit-image.png new file mode 100644 index 0000000000..a24becb110 Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/commit-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/developer-list.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/developer-list.png new file mode 100644 index 0000000000..87a7dfb948 Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/developer-list.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-image.png new file mode 100644 index 0000000000..fc8e7f1efe Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-profile.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-profile.png new file mode 100644 index 0000000000..b6ade4ca1e Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/fork-project-profile.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/git-push-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/git-push-image.png new file mode 100644 index 0000000000..ff3b37fef3 Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/git-push-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-change.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-change.png new file mode 100644 index 0000000000..92125966ec Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-change.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-pull-request.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-pull-request.png new file mode 100644 index 0000000000..44474b1967 Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/github-desktop-pull-request.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/open-pull-request-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/open-pull-request-image.png new file mode 100644 index 0000000000..3f8df381ce Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/open-pull-request-image.png differ diff --git a/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/pull-request-image.png b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/pull-request-image.png new file mode 100644 index 0000000000..9eaacaf3db Binary files /dev/null and b/docs/en/Community-Articles/2023-01-30-How-To-Contribute-To-a-Open-Source-Software/images/pull-request-image.png differ diff --git a/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/POST.md b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/POST.md new file mode 100644 index 0000000000..fb97c657f9 --- /dev/null +++ b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/POST.md @@ -0,0 +1,86 @@ +# Converting Create/Edit Modal to Page + +In this document we will explain how to convert BookStore's `Books` create & edit modals to regular razor pages. + +## Before +![before](images/old.gif) + +## Now +![after](images/new.gif) + +## Index page + +Repalce `abp-button(NewBookButton)` buttom with ` @L["NewBook"].Value`. + +## Index js file + +Remove the related codes of `createModal` and `editModal`. + +Change the `Edit row action` with `location.href = "/Books/EditModal?id=" + data.record.id;` + + +## Create/Edit Book page + +Remove `Layout = null;` and add some custom style and javascript code to `CreateModal.cshtml` & `EditModal.cshtml`. + +```csharp +@section styles { + +} +@section scripts { + +} +``` + +Add a `div` element with `abp-view-modal` class to wrap the `abp-dynamic-form`, Set size of `abp-modal` to `ExtraLarge` and remove the `AbpModalButtons.Cancel` button from `abp-modal-footer`. + +### CreateModal +```csharp +
+ + + + + + + + + +
+``` + +### EditModal +```csharp +
+ + + + + + + + + +
+``` + +You can check this Git commit for details. + +https://github.com/abpframework/abp-samples/commit/f3014e0ec422cb2d8816d0e00dd6ab9cc1adfc21 diff --git a/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/new.gif b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/new.gif new file mode 100644 index 0000000000..391963011c Binary files /dev/null and b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/new.gif differ diff --git a/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/old.gif b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/old.gif new file mode 100644 index 0000000000..a84714d4ee Binary files /dev/null and b/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/old.gif differ diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md index a3c9ae4e9a..c2cacb4e4c 100644 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -101,7 +101,7 @@ public class MyLocalIdentityUserChangeEventHandler : ```` * `EntityChangedEventData` 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` 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). diff --git a/docs/en/Dependency-Injection.md b/docs/en/Dependency-Injection.md index bfce8a71bf..206c2c58a4 100644 --- a/docs/en/Dependency-Injection.md +++ b/docs/en/Dependency-Injection.md @@ -2,7 +2,7 @@ ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too. -> While ABP has no core dependency to any 3rd-party DI provider, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with Autofac installed. See [Autofac integration](Autofac-Integration.md) document for more information. +> While ABP has no core dependency to any 3rd-party DI provider. However, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with [Autofac](https://autofac.org/) installed. See [Autofac integration](Autofac-Integration.md) document for more information. ## Modularity @@ -20,24 +20,24 @@ public class BlogModule : AbpModule ## Conventional Registration -ABP introduces conventional service registration. You need not do anything to register a service by convention. It's automatically done. If you want to disable it, you can set `SkipAutoServiceRegistration` to `true` by overriding the `PreConfigureServices` method. +ABP introduces conventional service registration. You need not do anything to register a service by convention. It's automatically done. If you want to disable it, you can set `SkipAutoServiceRegistration` to `true` in the constructor of your module class. Example: ````C# public class BlogModule : AbpModule { - public override void PreConfigureServices(ServiceConfigurationContext context) + public BlogModule() { SkipAutoServiceRegistration = true; } } ```` -Once you skip auto registration, you should manually register your services. In that case, ``AddAssemblyOf`` extension method can help you to register all your services by convention. Example: +Once you skip the auto registration, you should manually register your services. In that case, ``AddAssemblyOf`` extension method can help you to register all your services by convention. Example: ````c# public class BlogModule : AbpModule { - public override void PreConfigureServices(ServiceConfigurationContext context) + public BlogModule() { SkipAutoServiceRegistration = true; } @@ -105,9 +105,7 @@ Example: [Dependency(ServiceLifetime.Transient, ReplaceServices = true)] public class TaxCalculator { - } - ```` ``Dependency`` attribute has a higher priority than other dependency interfaces if it defines the ``Lifetime`` property. @@ -120,7 +118,6 @@ public class TaxCalculator [ExposeServices(typeof(ITaxCalculator))] public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency { - } ```` @@ -179,7 +176,11 @@ public class MyModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { //Replacing the IConnectionStringResolver service - context.Services.Replace(ServiceDescriptor.Transient()); + context.Services.Replace( + ServiceDescriptor.Transient< + IConnectionStringResolver, + MyConnectionStringResolver + >()); } } ```` @@ -202,7 +203,7 @@ public class TaxAppService : ApplicationService _taxCalculator = taxCalculator; } - public void DoSomething() + public async Task DoSomethingAsync() { //...use _taxCalculator... } @@ -227,7 +228,7 @@ public class MyService : ITransientDependency Logger = NullLogger.Instance; } - public void DoSomething() + public async Task DoSomethingAsync() { //...use Logger to write logs... } @@ -244,17 +245,21 @@ One restriction of property injection is that you cannot use the dependency in y Property injection is also useful when you want to design a base class that has some common services injected by default. If you're going to use constructor injection, all derived classes should also inject depended services into their own constructors which makes development harder. However, be very careful using property injection for non-optional services as it makes it harder to clearly see the requirements of a class. -#### DisablePropertyInjectionAttribute +#### DisablePropertyInjection Attribute -You can use `[DisablePropertyInjection]` attribute on class or properties to disable property injection for the whole class or some specific properties. +You can use `[DisablePropertyInjection]` attribute on classes or their properties to disable property injection for the whole class or some specific properties. ````C# +// Disabling for all properties of the MyService class [DisablePropertyInjection] public class MyService : ITransientDependency { + public ILogger Logger { get; set; } + public ITaxCalculator TaxCalculator { get; set; } } +// Disabling only for the TaxCalculator property public class MyService : ITransientDependency { public ILogger Logger { get; set; } @@ -262,17 +267,21 @@ public class MyService : ITransientDependency [DisablePropertyInjection] public ITaxCalculator TaxCalculator { get; set; } } - ```` ### Resolve Service from IServiceProvider -You may want to resolve a service directly from ``IServiceProvider``. In that case, you can inject IServiceProvider into your class and use ``GetService`` method as shown below: +You may want to resolve a service directly from ``IServiceProvider``. In that case, you can inject `IServiceProvider` into your class and use the ``GetService`` or the `GetRequiredService` method as shown below: ````C# public class MyService : ITransientDependency { - public ILogger Logger { get; set; } + private readonly ITaxCalculator _taxCalculator; + + public MyService(IServiceProvider serviceProvider) + { + _taxCalculator = serviceProvider.GetRequiredService(); + } } ```` @@ -374,15 +383,15 @@ IEnumerable services = _serviceProvider.GetServices(); + // TODO: Use the taxCalculator + } +} + +```` + +With such a usage, you don't need to deal with creating service scopes and disposing the resolved services (as explained in the *Releasing/Disposing Services* section above). Because all the services resolved from the `ICachedServiceProvider` will be released once the service scope of the `MyService` instance is disposed. Also, you don't need to care about memory leaks (because of creating too many `ITaxCalculator` instances if we call `DoSomethingAsync` too many times), because only one `ITaxCalculator` instance is created, and it is reused. + +Since `ICachedServiceProvider` and `ITransientCachedServiceProvider` extends the standard `IServiceProvider` interface, you can use all the extension method of the `IServiceProvider` interface on them. In addition, they provides some other methods to provide a default value or a factory method for the services that are not found (that means not registered to the dependency injection system). Notice that the default value (or the value returned from your factory method) is also cached and reused. + +Use `ICachedServiceProvider` (instead of `ITransientCachedServiceProvider`) unless you need to create the service cache per usage. `ITransientCachedServiceProvider` guarantees that the created service instances are not shared with any other service, even they are in the same service scope. The services resolved from `ICachedServiceProvider` are shared with other services in the same service scope (in the same HTTP Request, for example), so it can be thought as more optimized. + +> ABP Framework also provides the `IAbpLazyServiceProvider` service. It does exists for backward compatibility and works exactly same with the `ITransientCachedServiceProvider` service. So, use the `ITransientCachedServiceProvider` since the `IAbpLazyServiceProvider` might be removed in future ABP versions. ## Advanced Features -### IServiceCollection.OnRegistred Event +### IServiceCollection.OnRegistered Event -You may want to perform an action for every service registered to the dependency injection. In the `PreConfigureServices` method of your module, register a callback using the `OnRegistred` method as shown below: +You may want to perform an action for every service registered to the dependency injection. In the `PreConfigureServices` method of your module, register a callback using the `OnRegistered` method as shown below: ````csharp public class AppModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { - context.Services.OnRegistred(ctx => + context.Services.OnRegistered(ctx => { var type = ctx.ImplementationType; //... @@ -421,7 +465,7 @@ public class AppModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { - context.Services.OnRegistred(ctx => + context.Services.OnRegistered(ctx => { if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true)) { @@ -434,7 +478,7 @@ public class AppModule : AbpModule This example simply checks if the service class has `MyLogAttribute` attribute and adds `MyLogInterceptor` to the interceptor list if so. -> Notice that `OnRegistred` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](Dynamic-Proxying-Interceptors.md) of dynamic proxying / interceptors. +> Notice that `OnRegistered` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](Dynamic-Proxying-Interceptors.md) of dynamic proxying / interceptors. ## 3rd-Party Providers diff --git a/docs/en/Deployment/Clustered-Environment.md b/docs/en/Deployment/Clustered-Environment.md index 2dace9a7f7..e3294b3469 100644 --- a/docs/en/Deployment/Clustered-Environment.md +++ b/docs/en/Deployment/Clustered-Environment.md @@ -63,7 +63,7 @@ The [Database BLOB provider](../Blob-Storing-Database) is the easiest way since > [ABP Commercial](https://commercial.abp.io/) startup solution templates come with the database BLOB provider as pre-installed, and stores BLOBs in the application's database. -Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOG storage providers. +Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOB storage providers. ## Configuring Background Jobs diff --git a/docs/en/Deployment/Configuring-Production.md b/docs/en/Deployment/Configuring-Production.md new file mode 100644 index 0000000000..db928c8590 --- /dev/null +++ b/docs/en/Deployment/Configuring-Production.md @@ -0,0 +1,104 @@ +# Configuring Your Application for Production Environments + +ABP Framework has a lot of options to configure and fine-tune its features. They are all explained in their own documents. Default values for these options are pretty well for most of the deployment environments. However, you may need to care about some options based on how you've structured your deployment environment. In this document, we will highlight these kind of options. So, it is highly recommended to read this document in order to not have unexpected behaviors in your system in production. + +## Distributed Cache Prefix + +ABP's [distributed cache infrastructure](../Caching.md) provides an option to set a key prefix for all of your data that is saved into your distributed cache provider. The default value of this option is not set (it is `null`). If you are using a distributed cache server that is shared by different applications, then you can set a prefix value to isolate an application's cache data from others. + +````csharp +Configure(options => +{ + options.KeyPrefix = "MyCrmApp"; +}); +```` + +That's all. ABP, then will add this prefix to all of your cache keys in your application as along as you use ABP's `IDistributedCache` or `IDistributedCache` services. See the [Caching documentation](../Caching.md) if you are new to distributed caching. + +> **Warning**: If you use ASP.NET Core's standard `IDistributedCache` service, it's your responsibility to add the key prefix (you can get the value by injecting `IOptions`). ABP can not do it. + +> **Warning**: Even if you have never used distributed caching in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your caching server is shared among multiple systems. + +> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed cache server. In such systems, all applications (or services) should normally use the same cache prefix, because you want all the applications to use the same cache data to have consistency between them. + +> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for the distributed cache. So, please check your application code if it is already configured. + +## Distributed Lock Prefix + +ABP's [distributed locking infrastructure](../Distributed-Locking.md) provides an option to set a prefix for all the keys you are using in the distributed lock server. The default value of this option is not set (it is `null`). If you are using a distributed lock server that is shared by different applications, then you can set a prefix value to isolate an application's lock from others. + +````csharp +Configure(options => +{ + options.KeyPrefix = "MyCrmApp"; +}); +```` + +That's all. ABP, then will add this prefix to all of your keys in your application. See the [Distributed Locking documentation](../Distributed-Locking.md) if you are new to distributed locking. + +> **Warning**: Even if you have never used distributed locking in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your distributed lock server is shared among multiple systems. + +> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed locking server. In such systems, all applications (or services) should normally use the same lock prefix, because you want to globally lock your resources in your system. + +> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for distributed locking. So, please check your application code if it is already configured. + +## Email Sender + +ABP's [Email Sending](../Emailing.md) system abstracts sending emails from your application and module code and allows you to configure the email provider and settings in a single place. + +Email service is configured to write email contents to the standard [application log](../Logging.md) in development environment. You should configure the email settings to be able to send emails to users in your production environment. + +Please see the [Email Sending](../Emailing.md) document to learn how to configure its settings to really send emails. + +> **Warning**: If you don't configure the email settings, you will get errors while trying to send emails. For example, the [Account module](../Modules/Account.md)'s *Password Reset* feature sends email to the users to reset their passwords if they forget it. + +## SMS Sender + +ABP's [SMS Sending abstraction](https://docs.abp.io/en/abp/latest/SMS-Sending) provides a unified interface to send SMS to users. However, its implementation is left to you. Because, typically a paid SMS service is used to send SMS, and ABP doesn't depend on a specific SMS provider. + +So, if you are using the `ISmsSender` service, you must implement it yourself, as shown in the following code block: + +````csharp +public class MySmsSender : ISmsSender, ITransientDependency +{ + public async Task SendAsync(SmsMessage smsMessage) + { + // TODO: Send it using your provider... + } +} +```` + +> [ABP Commercial](https://commercial.abp.io/) provides a [Twilio SMS Module](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) as a pre-built integration with the popular [Twilio](https://www.twilio.com/) platform. + +## BLOB Provider + +If you use ABP's [BLOB Storing](https://docs.abp.io/en/abp/latest/Blob-Storing) infrastructure, you should care about the BLOB provider in your production environment. For example, if you use the [File System](../Blob-Storing-File-System.md) provider and your application is running in a Docker container, you should configure a volume mapping for the BLOB storage path. Otherwise, your data will be lost when the container is restarted. Also, the File System is not a good provider for production if you have a [clustered deployment](Clustered-Environment.md) or a microservice system. + +Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOB storage providers. + +> **Warning**: Even if you don't directly use the BLOB Storage system, a module you are depending on may use it. For example, ABP Commercial's [File Management](https://docs.abp.io/en/commercial/latest/modules/file-management) module stores file contents, and the [Account](https://docs.abp.io/en/commercial/latest/modules/account) module stores user profile pictures in the BLOB Storage system. So, be careful with the BLOB Storing configuration in production. Note that ABP Commercial uses the [Database Provider](../Blob-Storing-Database.md) as a pre-configured BLOB storage provider, which works in production without any problem, but you may still want to use another provider. + +## String Encryption + +ABP's [`IStringEncryptionService` Service](../String-Encryption.md) simply encrypts and decrypts given strings based on a password phrase. You should configure the `AbpStringEncryptionOptions` options for the production with a strong password and keep it as a secret. You can also configure the other properties of those options class. See the following example: + +````csharp +Configure(options => +{ + options.DefaultPassPhrase = "gs5nTT042HAL4it1"; +}); +```` + +Note that ABP CLI automatically sets the password to a random value on a new project creation. However, it is stored in the `appsettings.json` file and is generally added to your source control. It is suggested to use [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) or [Environment Variables](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) to set that value. + +## Logging + +ABP uses .NET's standard [Logging services](../Logging.md). So, it is compatible with any logging provider that works with .NET. ABP's startup solution templates come with [Serilog](https://serilog.net/) pre-installed and configured for you. It writes logs to file system and console with the initial configuration. File system is useful for development environment, but it is suggested you to use a different provider for your production environment, like Elasticsearch, database or any other provider that can properly work. + +## The Swagger UI + +ABP's startup solution templates come with [Swagger UI](https://swagger.io/) pre-installed. Swagger is a pretty standard and useful tool to discover and test your HTTP APIs on a built-in UI that is embedded into your application or service. It is typically used in development environment, but you may want to enable it on staging or production environments too. + +While you will always secure your HTTP APIs with other techniques (like the [Authorization](../Authorization.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment. + +> You may also want to see the [ABP Swagger integration](../API/Swagger-Integration.md) document. diff --git a/docs/en/Deployment/Distributed-Microservice.md b/docs/en/Deployment/Distributed-Microservice.md new file mode 100644 index 0000000000..9af4a1c839 --- /dev/null +++ b/docs/en/Deployment/Distributed-Microservice.md @@ -0,0 +1,45 @@ +# Deploying Distributed / Microservice Solutions + +The ABP Framework is designed to consider distributed and microservice systems, where you have multiple applications and/or services communicating internally. All of its features are compatible with distributed scenarios. This document highlights some points you should care about when you deploy your distributed or microservice solution. + +## Application Name & Instance Id + +ABP provides the `IApplicationInfoAccessor` service that provides the following properties: + +* `ApplicationName`: A human-readable name for an application. It is a unique value for an application. +* `InstanceId`: A random (GUID) value generated by the ABP Framework each time you start the application. + +These values are used by the ABP Framework in several places to distinguish the application and the application instance (process) in the system. For example, the [audit logging](../Audit-Logging.md) system saves the `ApplicationName` in each audit log record written by the related application, so you can understand which application has created the audit log entry. So, if your system consists of multiple applications saving audit logs to a single point, you should be sure that each application has a different `ApplicationName`. + +The `ApplicationName` property's value is set automatically from the **entry assembly's name** (generally, the project name in a .NET solution) by default, which is proper for most cases, since each application typically has a unique entry assembly name. + +There are two ways to set the application name to a different value. In this first approach, you can set the `ApplicationName` property in your application's [configuration](../Configuration.md). The easiest way is to add an `ApplicationName` field to your `appsettings.json` file: + +````json +{ + "ApplicationName": "Services.Ordering" +} +```` + +Alternatively, you can set `AbpApplicationCreationOptions.ApplicationName` while creating the ABP application. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `ApplicationName` option as shown below: + +````csharp +await builder.AddApplicationAsync(options => +{ + options.ApplicationName = "Services.Ordering"; +}); +```` + +## Using a Distributed Event Bus + +ABP's [Distributed Event Bus](../Distributed-Event-Bus.md) system provides a standard interface to communicate with other applications and services. While the name is "distributed", the default implementation is in-process. That means, your applications / services can not communicate with each other unless you explicitly configure a distributed event bus provider. + +If you are building a distributed system, then the applications should communicate through an external distributed messaging server. Please follow the [Distributed Event Bus](../Distributed-Event-Bus.md) document to learn how to install and configure your distributed event bus provider. + +> **Warning**: Even if you don't use the distributed event bus directly in your application code, the ABP Framework and some of the modules you are using may use it. So, if you are building a distributed system, always configure a distributed event bus provider. + +> **Info**: [Clustered deployment](Clustered-Environment.md) of a single application is not considered as a distributed system. So, if you only have a single application with multiple instances serving behind a load balancer, a real distributed messaging server may not be needed. + +## See Also + +* [Deploying to a Clustered Environment](Clustered-Environment.md) diff --git a/docs/en/Deployment/Index.md b/docs/en/Deployment/Index.md index 6503f8b664..849d3bd4f3 100644 --- a/docs/en/Deployment/Index.md +++ b/docs/en/Deployment/Index.md @@ -6,4 +6,7 @@ However, there are some topics that you should care about when you are deploying ## Guides -* [Deploying to a clustered environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently. +* [Configuring for Production](Configuring-Production.md): Notes for some essential configurations for production environments. +* [Optimization for Production](Optimizing-Production.md): Tips and suggestions for optimizing your application on production environments. +* [Deploying to a Clustered Environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently. +* [Deploying Distributed / Microservice Solutions](Distributed-Microservice.md): Deployment notes for solutions consisting of multiple applications and/or services. diff --git a/docs/en/Deployment/Optimizing-Production.md b/docs/en/Deployment/Optimizing-Production.md new file mode 100644 index 0000000000..7e896f9b1c --- /dev/null +++ b/docs/en/Deployment/Optimizing-Production.md @@ -0,0 +1,22 @@ +# Optimizing Your Application for Production Environments + +ABP Framework and the startup solution templates are configured well to get the maximum performance on production environments. However, there are still some points you need to pay attention to in order to optimize your system in production. In this document, we will mention some of these topics. + +## Caching Static Contents + +The following items are contents that can be cached in the client side (typically in the Browser) or in a CDN server: + +* **Static images** can always be cached. Here, you should be careful that if you change an image, use a different file name, or use a versioning query-string parameter, so the browser (or CDN) understands it's been changed. +* **CSS and JavaScript files**. ABP's [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) system always uses a query-string versioning parameter and a hash value in the files names of the CSS & JavaScript files for the [MVC (Razor Pages)](../UI/AspNetCore/Overall.md) UI. So, you can safely cache these files in the client side or in a CDN server. +* **Application bundle files** of an [Angular UI](../UI/Angular/Quick-Start.md) application. +* **[Application Localization Endpoint](../API/Application-Localization.md)** can be cached per culture (it already has a `cultureName` query string parameter) if you don't use dynamic localization on the server-side. ABP Commercial's [Language Management](https://commercial.abp.io/modules/Volo.LanguageManagement) module provides dynamic localization. If you're using it, you can't cache that endpoint forever. However, you can still cache it for a while. Applying dynamic localization text changes to the application can delay for a few minutes, even for a few hours in a real life scenario. + +There may be more ways based on your solution structure and deployment environment, but these are the essential points you should consider to client-side cache in a production environment. + +## Bundling & Minification for MVC (Razor Pages) UI + +ABP's [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) system automatically bundles, minifies and versions your CSS and JavaScript files in production environment. Normally, you don't need to do anything, if you haven't disabled it yourself in your application code. It is important to follow the [bundling & minification](../UI/AspNetCore/Bundling-Minification.md) document and truly use the system to get the maximum optimization. + +## Background Jobs + +ABP's [Background Jobs](../Background-Jobs.md) system provides an abstraction with a basic implementation to enqueue jobs and execute them in a background thread. ABP's Default Background Job Manager may not be enough if you are adding too many jobs to the queue and want them to be executed in parallel by multiple servers with a high performance. If you need these, you should consider to configure a dedicated background job software, like [Hangfire](https://www.hangfire.io/). ABP has a pre-built [Hangfire integration](../Background-Jobs-Hangfire.md), so you can switch to Hangfire without changing your application code. diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 21b1b07256..f1b8464d59 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/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: + +````csharp +[EventName("product")] +public class ProductEto : EntityEto +{ + // 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(options => +{ + options.AutoEventSelectors.Add(); + options.EtoMappings.Add(); +}); +```` + +Finally, you should create a class in the Ordering microservice, that is derived from the `EntitySynchronizer` class: + +````csharp +public class ProductSynchronizer : EntitySynchronizer +{ + public ProductSynchronizer( + IObjectMapper objectMapper, + IRepository 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` 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 +{ + public ProductSynchronizer( + IObjectMapper objectMapper, + IRepository 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. @@ -434,6 +502,29 @@ Here, the following properties are available on the `config` object: * `HandlerSelector`: A predicate to filter the event handled types (classes implementing the `IDistributedEventHandler` interface) to be used for this configuration. This is especially useful if you want to ignore some event handler types from inbox processing, or want to define named inbox configurations and group event handlers within these configurations. See the *Named Configurations* section. * `ImplementationType`: Type of the class that implements the database operations for the inbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages. +#### AbpEventBusBoxesOptions + +`AbpEventBusBoxesOptions` can be used to fine-tune how inbox and outbox systems work. For most of the systems, using the defaults would be more than enough, but you can configure it to optimize your system when it is needed. + +Just like all the [options classes](Options.md), `AbpEventBusBoxesOptions` can be configured in the `ConfigureServices` method of your [module class](Module-Development-Basics.md) as shown in the following code block: + +````csharp +Configure(options => +{ + // TODO: configure the options +}); +```` + +`AbpEventBusBoxesOptions` has the following properties to be configured: + +* `BatchPublishOutboxEvents`: Can be used to enable or disable batch publishing events to the message broker. Batch publishing works if it is supported by the distributed event bus provider. If not supported, events are sent one by one as the fallback logic. Keep it as enabled since it has a great performance gain wherever possible. Default value is `true` (enabled). +* `PeriodTimeSpan`: The period of the inbox and outbox message processors to check if there is a new event in the database. Default value is 2 seconds (`TimeSpan.FromSeconds(2)`). +* `CleanOldEventTimeIntervalSpan`: The event inbox system periodically checks and deletes the old processed events from the inbox in the database. You can set this value to determine the check period. Default value is 6 hours (`TimeSpan.FromHours(6)`). +* `WaitTimeToDeleteProcessedInboxEvents`: Inbox events are not deleted from the database for a while even if they are successfully processed. This is for a system to prevent multiple process of the same event (if the event broker sends it twice). This configuration value determines the time to keep the processed events. Default value is 2 hours (`TimeSpan.FromHours(2)`). +* `InboxWaitingEventMaxCount`: The maximum number of events to query at once from the inbox in the database. Default value is 1000. +* `OutboxWaitingEventMaxCount`: The maximum number of events to query at once from the outbox in the database. Default value is 1000. +* `DistributedLockWaitDuration`: ABP uses [distributed locking](Distributed-Locking.md) to prevent concurrent access to the inbox and outbox messages in the database, when running multiple instance of the same application. If an instance of the application can not obtain the lock, it tries after a duration. This is the configuration of that duration. Default value is 15 seconds (`TimeSpan.FromSeconds(15)`). + ### Skipping Outbox `IDistributedEventBus.PublishAsync` method provides an optional parameter, `useOutbox`, which is set to `true` by default. If you bypass outbox and immediately publish an event, you can set it to `false` for a specific event publishing operation. diff --git a/docs/en/Entities.md b/docs/en/Entities.md index ef8f52a04f..124e0adab1 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -316,6 +316,29 @@ All these base classes also have non-generic versions to take `AuditedEntity` an All these base classes also have `...WithUser` pairs, like `FullAuditedAggregateRootWithUser` and `FullAuditedAggregateRootWithUser`. This makes possible to add a navigation property to your user entity. However, it is not a good practice to add navigation properties between aggregate roots, so this usage is not suggested (unless you are using an ORM, like EF Core, that well supports this scenario and you really need it - otherwise remember that this approach doesn't work for NoSQL databases like MongoDB where you must truly implement the aggregate pattern). Also, if you add navigation properties to the AppUser class that comes with the startup template, consider to handle (ignore/map) it on the migration dbcontext (see [the EF Core migration document](Entity-Framework-Core-Migrations.md)). +## Caching Entities + +ABP Framework provides a [Distributed Entity Cache System](Entity-Cache.md) for caching entities. It is useful if you want to use caching for quicker access to the entity rather than repeatedly querying it from the database. + +It's designed as read-only and automatically invalidates a cached entity if the entity is updated or deleted. + +> 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. diff --git a/docs/en/Entity-Cache.md b/docs/en/Entity-Cache.md new file mode 100644 index 0000000000..834a0d5313 --- /dev/null +++ b/docs/en/Entity-Cache.md @@ -0,0 +1,131 @@ +# Entity Cache + +ABP Framework provides an entity caching system that works on top of the [distributed caching](Caching.md) system. It does the following operations on behalf of you: + +* Gets the entity from the database (by using the [repositories](Repositories.md)) in its first call and then gets it from the cache in subsequent calls. +* Automatically invalidates the cached entity if the entity is updated or deleted. Thus, it will be retrieved from the database in the next call and will be re-cached. + +## Caching Entity Objects + +`IEntityCache` is a simple service provided by the ABP Framework for caching entities. Assume that you have a `Product` entity as shown below: + +```csharp +public class Product : AggregateRoot +{ + public string Name { get; set; } + public string Description { get; set; } + public float Price { get; set; } + public int StockCount { get; set; } +} +``` + +If you want to cache this entity, you should first configure the [dependency injection](Dependency-Injection.md) system to register the `IEntityCache` service in the `ConfigureServices` method of your [module class](Module-Development-Basics.md): + +```csharp +context.Services.AddEntityCache(); +``` + +Now you can inject the `IEntityCache` service wherever you need: + +```csharp +public class ProductAppService : ApplicationService, IProductAppService +{ + private readonly IEntityCache _productCache; + + public ProductAppService(IEntityCache productCache) + { + _productCache = productCache; + } + + public async Task GetAsync(Guid id) + { + var product = await _productCache.GetAsync(id); + return ObjectMapper.Map(product); + } +} +``` + +> Note that we've used the `ObjectMapper` service to map from `Product` to `ProductDto`. You should configure that [object mapping](Object-To-Object-Mapping.md) to make that example service properly works. + +That's all. The cache name (in the distributed cache server) will be full name (with namespace) of the `Product` class. You can use the `[CacheName]` attribute to change it. Please refer to the [caching document](Caching.md) for details. + +## Using a Cache Item Class + +In the previous section, we've directly cached the `Product` entity. In that case, the `Product` class must be serializable to JSON (and deserializable from JSON). Sometimes that might not be possible or you may want to use another class to store the cache data. For example, we may want to use the `ProductDto` class instead of the `Product` class for the cached object if the `Product` entity. + +Assume that we've created a `ProductDto` class as shown below: + +```csharp +public class ProductDto : EntityDto +{ + public string Name { get; set; } + public string Description { get; set; } + public float Price { get; set; } + public int StockCount { get; set; } +} +``` + +Now, we can register the entity cache services to [dependency injection](Dependency-Injection.md) in the `ConfigureServices` method of your [module class](Module-Development-Basics.md) with three generic parameters, as shown below: + +```csharp +context.Services.AddEntityCache(); +``` + +Since the entity cache system will perform the [object mapping](Object-To-Object-Mapping.md) (from `Product` to `ProductDto`), we should configure the object map. Here, an example configuration with [AutoMapper](https://automapper.org/): + +```csharp +public class MyMapperProfile : Profile +{ + public MyMapperProfile() + { + CreateMap(); + } +} +``` + +Now, you can inject the `IEntityCache` service wherever you want: + +```csharp +public class ProductAppService : ApplicationService, IProductAppService +{ + private readonly IEntityCache _productCache; + + public ProductAppService(IEntityCache productCache) + { + _productCache = productCache; + } + + public async Task GetAsync(Guid id) + { + return await _productCache.GetAsync(id); + } +} +``` + +Notice that the `_productCache.GetAsync` method already returns a `ProductDto` object, so we could directly return it from out application service. + +## Configuration + +All of the `context.Services.AddEntityCache()` methods get an optional `DistributedCacheEntryOptions` parameter where you can easily configure the caching options: + +```csharp +context.Services.AddEntityCache( + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(30) + } +); +``` + +> The default cache duration is **2 minutes** with the `AbsoluteExpirationRelativeToNow` configuration. + +## Additional Notes + +* Entity classes should be serializable/deserializable to/from JSON to be cached (because it's serialized to JSON when saving in the [Distributed Cache](Caching.md)). If your entity class is not serializable, you can consider using a cache-item/DTO class instead, as explained before. +* Entity Caching System is designed as **read-only**. You should use the standard [repository](Repositories.md) methods to manipulate the entity if you need. If you need to manipulate (update) the entity, do not get it from the entity cache. Instead, read it from the repository, change it and update using the repository. + +## See Also + +* [Distributed caching](Caching.md) +* [Entities](Entities.md) +* [Repositories](Repositories.md) diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 568623a15d..fc17ddcd49 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -834,7 +834,7 @@ One advantage of using an interface for a DbContext is then it will be replaceab Once you properly define and use an interface for DbContext, then any other implementation can use the following ways to replace it: -**ReplaceDbContextAttribute** +#### ReplaceDbContext Attribute ```csharp [ReplaceDbContext(typeof(IBookStoreDbContext))] @@ -844,7 +844,7 @@ public class OtherDbContext : AbpDbContext, IBookStoreDbContext } ``` -**ReplaceDbContext option** +#### ReplaceDbContext Option ````csharp context.Services.AddAbpDbContext(options => @@ -856,6 +856,22 @@ context.Services.AddAbpDbContext(options => In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. +#### Replacing with Multi-Tenancy + +It is also possible to replace a DbContext based on the [multi-tenancy](Multi-Tenancy.md) side. `ReplaceDbContext` attribute and `ReplaceDbContext` method can get a `MultiTenancySides` option with a default value of `MultiTenancySides.Both`. + +**Example:** Replace DbContext only for tenants, using the `ReplaceDbContext` attribute + +````csharp +[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)] +```` + +**Example:** Replace DbContext only for the host side, using the `ReplaceDbContext` method + +````csharp +options.ReplaceDbContext(MultiTenancySides.Host); +```` + ### Split Queries ABP enables [split queries](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries) globally by default for better performance. You can change it as needed. diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index c7f329878c..117b0b499e 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -1,4 +1,4 @@ -# Getting Started With an ABP and AspNet Core MVC Web Application +# Getting Started with an Empty ASP.NET Core MVC / Razor Pages Application This tutorial explains how to start ABP from scratch with minimal dependencies. You generally want to start with the **[startup template](Getting-Started-AspNetCore-MVC-Template.md)**. diff --git a/docs/en/Getting-Started-React-Native.md b/docs/en/Getting-Started-React-Native.md index fe4cf478de..be0505b9f1 100644 --- a/docs/en/Getting-Started-React-Native.md +++ b/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(options => { + options.UseAspNetCore() + .DisableTransportSecurityRequirement(); + }); +#endif +``` + ## How to Configure & Run the React Native Application diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md index 44fbd3eb03..36cd5ffe0a 100644 --- a/docs/en/Getting-Started-Running-Solution.md +++ b/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 }} diff --git a/docs/en/Getting-Started-Setup-Environment.md b/docs/en/Getting-Started-Setup-Environment.md index 1a47c67d34..f6df09badf 100644 --- a/docs/en/Getting-Started-Setup-Environment.md +++ b/docs/en/Getting-Started-Setup-Environment.md @@ -19,7 +19,7 @@ First things first! Let's setup your development environment before creating the The following tools should be installed on your development machine: -* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 6.0+](https://dotnet.microsoft.com/download/dotnet) development. +* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 7.0+](https://dotnet.microsoft.com/download/dotnet) development. {{ if UI != "Blazor" }} * [Node v16 or v18](https://nodejs.org/) * [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [1](#f-yarn) or npm v6+ (already installed with Node) diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md index 0304eb2320..6352f43bc7 100644 --- a/docs/en/Getting-Started.md +++ b/docs/en/Getting-Started.md @@ -11,8 +11,6 @@ > This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. -## Contents - This tutorial explains how to **create and run** a new web application using the ABP Framework. Follow the steps below; 1. [Setup your development environment](Getting-Started-Setup-Environment.md) diff --git a/docs/en/Integration-Services.md b/docs/en/Integration-Services.md new file mode 100644 index 0000000000..966b383ef7 --- /dev/null +++ b/docs/en/Integration-Services.md @@ -0,0 +1,80 @@ +# Integration Services + +The *Integration Service* concept was created to distinguish the [application services](Application-Services.md) that are built for inter-module (or inter-microservice) communication from the application services that are intended to be consumed from a UI or a client application. + +The following figure shows a few microservices behind an API Gateway that is consumed by a UI application and 3rd-party client applications: + +![integration-services](images/integration-services.png) + +HTTP requests coming from out of the API Gateway can be called as *external request*, while the HTTP requests performed between microservices can be considered as *internal requests*. The application services that are designed to respond to these external requests are called as *integration services*, because their purpose is to integrate microservices in the system, rather than respond to user requests. + +## Marking an Application Service as Integration Service + +Assume that you have an application service named `ProductAppService`, and you want to use that application service as an integration service. In that case, you can use the `[IntegrationService]` attribute on top of the application service class as shown below: + +```csharp +[IntegrationService] +public class ProductAppService : ApplicationService, IProductAppService +{ + // ... +} +``` + +If your application service has an interface, like `IProductService` in this example, you can use it on the service interface: + +```csharp +[IntegrationService] +public interface IProductAppService : IApplicationService +{ + // ... +} +``` + +> If you've used the `[IntegrationService]` on top of your service interface, it is *not needed* to use on the service class too. + +That's all. From now, ABP will handle your application service as integration service and implement the followings by convention: + +* If you are using the [Auto API Controllers](API/Auto-API-Controllers.md) feature in your application, the URL prefix will be `/integration-api` instead of `/api` for your integration services. Thus, you can distinguish internal and external service communications and take additional actions, such as preventing REST API calls for integration services out of API Gateway. +* Audit logging is disabled by default for the integration services. See the next section if you want to enable it. + +## Configuration + +### Enabling/Disabling the Audit Logging + +Audit Logging is disabled by default for integration services but it can be enabled by configuring the `AbpAuditingOptions` [options class](Options.md) in the `ConfigureServices` method of your [module class](Module-Development-Basics.md): + +```csharp +Configure(options => +{ + options.IsEnabledForIntegrationService = true; +}); +``` + +> Please refer to the [audit logging document](Audit-Logging.md) for other options and details. + +### Filtering Auto API Controllers + +You can filter integration services (or non-integration services) while creating [Auto API Controllers](API/Auto-API-Controllers.md), using the `ApplicationServiceTypes` option of the `ConventionalControllerSetting` by configuring the `AbpAspNetCoreMvcOptions` as shown below: + +```csharp +Configure(options => +{ + options.ConventionalControllers.Create( + typeof(MyApplicationModule).Assembly, + conventionalControllerSetting => + { + conventionalControllerSetting.ApplicationServiceTypes = + ApplicationServiceTypes.IntegrationServices; + }); +}); +``` + +Tip: You can call the `options.ConventionalControllers.Create` multiple times to configure regular application services and integration services with different options. + +> Please refer to the [Auto API Controllers document](API/Auto-API-Controllers.md) for more information about the Auto API Controller system. + +## See Also + +* [Application Services](Application-Services.md) +* [Auto API Controllers](API/Auto-API-Controllers.md) +* [Audit Logging](Audit-Logging.md) \ No newline at end of file diff --git a/docs/en/JSON.md b/docs/en/JSON.md index 27693bafda..d4fc550982 100644 --- a/docs/en/JSON.md +++ b/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`) + diff --git a/docs/en/KB/Windows-Path-Too-Long-Fix.md b/docs/en/KB/Windows-Path-Too-Long-Fix.md new file mode 100644 index 0000000000..e463288239 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-7_0.md b/docs/en/Migration-Guides/Abp-7_0.md index 3b6b1c5e6a..65a1614603 100644 --- a/docs/en/Migration-Guides/Abp-7_0.md +++ b/docs/en/Migration-Guides/Abp-7_0.md @@ -101,3 +101,113 @@ See https://github.com/abpframework/abp/pull/13644 for more info. We've already done this for our themes. See https://github.com/abpframework/abp/pull/13845 for more info. + +## Replaced `BlogPostPublicDto` with `BlogPostCommonDto` + +- In the CMS Kit Module, `BlogPostPublicDto` has been moved to `Volo.CmsKit.Common.Application.Contracts` from `Volo.CmsKit.Public.Application.Contracts` and renamed to `BlogPostCommonDto`. + +- See the [PR#13499](https://github.com/abpframework/abp/pull/13499) for more information. + +> You can ignore this if you don't use CMS Kit Module. + +## Data migration environment + +Please call `AddDataMigrationEnvironment` method in the migration project. + +```cs +using (var application = await AbpApplicationFactory.CreateAsync(options => +{ + //... + options.AddDataMigrationEnvironment(); +})) +{ + //... +} +``` + +```cs +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDataMigrationEnvironment(); +// Call AddDataMigrationEnvironment before AddApplicationAsync +await builder.AddApplicationAsync(); +//... +``` + +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" }, + diff --git a/docs/en/Migration-Guides/Abp-7_1.md b/docs/en/Migration-Guides/Abp-7_1.md new file mode 100644 index 0000000000..7e579af217 --- /dev/null +++ b/docs/en/Migration-Guides/Abp-7_1.md @@ -0,0 +1,19 @@ +# ABP Version 7.1 Migration Guide + +This document is a guide for upgrading ABP v7.0 solutions to ABP v7.1. There are a few changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. + +## Navigation Menu - `CustomData` type changed to `Dictionary` + +`ApplicationMenu` and `ApplicationMenuItem` classes' `CustomData` property type has been changed to `Dictionary`. So, if you use the optional `CustomData` property of these classes, change it accordingly. See [#15608](https://github.com/abpframework/abp/pull/15608) for more information. + +*Old usage:* + +```csharp +var menu = new ApplicationMenu("Home", L["Home"], "/", customData: new MyCustomData()); +``` + +*New usage:* + +```csharp +var menu = new ApplicationMenu("Home", L["Home"], "/").WithCustomData("CustomDataKey", new MyCustomData()); +``` \ No newline at end of file diff --git a/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md b/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md new file mode 100644 index 0000000000..aff389ccf8 --- /dev/null +++ b/docs/en/Migration-Guides/IdentityServer4-Step-by-Step.md @@ -0,0 +1,230 @@ +# Migrating from OpenIddict to IdentityServer4 Step by Step Guide + +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 the default OpenID provider library for ABP templates since v6.0, you may want to keep using [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) library, even it is **archived and no longer maintained by the owners**. ABP doesn't provide support for newer versions of IdentityServer. This guide provides layer-by-layer guidance for migrating your existing [OpenIddict](https://github.com/openiddict/openiddict-core) application to IdentityServer4. + +## IdentityServer4 Migration Steps + +Use the `abp update` command to update your existing application. See [Upgrading docs](../Upgrading.md) for more info. Apply required migrations by following the [Migration Guides](Index.md) based on your application version. + +### Domain.Shared Layer + +- In **MyApplication.Domain.Shared.csproj** replace **project reference**: + +```csharp + +``` + + with + +```csharp + +``` + +- In **MyApplicationDomainSharedModule.cs** replace usings and **module dependencies:** + +```csharp +using Volo.Abp.OpenIddict; +... +typeof(AbpOpenIddictDomainSharedModule) +``` + + with + +```csharp +using Volo.Abp.IdentityServer; +... +typeof(AbpIdentityServerDomainSharedModule) +``` + +### Domain Layer + +- In **MyApplication.Domain.csproj** replace **project references**: + +```csharp + + +``` + + with + +```csharp + + +``` + +- In **MyApplicationDomainModule.cs** replace usings and **module dependencies**: + +```csharp +using Volo.Abp.OpenIddict; +using Volo.Abp.PermissionManagement.OpenIddict; +... +typeof(AbpOpenIddictDomainModule), +typeof(AbpPermissionManagementDomainOpenIddictModule), +``` + + with + +```csharp +using Volo.Abp.IdentityServer; +using Volo.Abp.PermissionManagement.IdentityServer; +... +typeof(AbpIdentityServerDomainModule), +typeof(AbpPermissionManagementDomainIdentityServerModule), +``` + +#### OpenIddictDataSeedContributor + +DataSeeder is the most important part for starting the application since it seeds the initial data for both OpenID providers. + +- Create a folder named *IdentityServer* under the Domain project and copy the [IdentityServerDataSeedContributor.cs](https://github.com/abpframework/abp-samples/blob/master/Ids2OpenId/src/Ids2OpenId.Domain/IdentityServer/IdentityServerDataSeedContributor.cs) under this folder. **Rename** all the `OpenId2Ids` with your project name. +- Delete *OpenIddict* folder that contains `OpenIddictDataSeedContributor.cs` which is no longer needed. + +### EntityFrameworkCore Layer + +If you are using MongoDB, skip this step and check the *MongoDB* layer section. + +- In **MyApplication.EntityFrameworkCore.csproj** replace **project reference**: + + ```csharp + + ``` + + with + + ```csharp + + ``` + +- In **MyApplicationEntityFrameworkCoreModule.cs** replace usings and **module dependencies**: + +```csharp +using Volo.Abp.OpenIddict.EntityFrameworkCore; +... +typeof(AbpOpenIddictEntityFrameworkCoreModule), +``` + + with + +```csharp +using Volo.Abp.IdentityServer.EntityFrameworkCore; +... +typeof(AbpIdentityServerEntityFrameworkCoreModule), +``` + +- In **MyApplicationDbContext.cs** replace usings and **fluent api configurations**: + + ```csharp + using Volo.Abp.OpenIddict.EntityFrameworkCore; + ... + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + ... + builder.ConfigureOpenIddict(); + ``` + + with + + ```csharp + using Volo.Abp.IdentityServer.EntityFrameworkCore; + ... + using Volo.Abp.OpenIddict.EntityFrameworkCore; + ... + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + ... + builder.ConfigureIdentityServer(); + ``` + +> Not: You need to create new migration after updating the fluent api. Navigate to *EntityFrameworkCore* folder and add a new migration. Ex, `dotnet ef migrations add Updated_To_IdentityServer ` + +### MongoDB Layer + +If you are using EntityFrameworkCore, skip this step and check the *EntityFrameworkCore* layer section. + +- In **MyApplication.MongoDB.csproj** replace **project reference**: + + ```csharp + + ``` + + with + + ```csharp + + ``` + +- In **MyApplicationMongoDbModule.cs** replace usings and **module dependencies**: + +```csharp +using Volo.Abp.OpenIddict.MongoDB; +... +typeof(AbpOpenIddictMongoDbModule), +``` + + with + +```csharp +using Volo.Abp.IdentityServer.MongoDB; +... +typeof(AbpIdentityServerMongoDbModule), +``` + +### DbMigrator Project + +- In `appsettings.json` **replace OpenIddict section with IdentityServer** since IdentityServerDataSeeder will be using these information for initial data seeding: + + ```json + "IdentityServer": { // Rename OpenIddict to IdentityServer + "Clients ": { // Rename Applications to Clients + ... + } + } + ``` + + +### Test Project + +- In **MyApplicationTestBaseModule.cs** **add** the IdentityServer related using and PreConfigurations: + + ```csharp + using Volo.Abp.IdentityServer; + ``` + + and + + ```csharp + PreConfigure(options => + { + options.AddDeveloperSigningCredential = false; + }); + + PreConfigure(identityServerBuilder => + { + identityServerBuilder.AddDeveloperSigningCredential(false, System.Guid.NewGuid().ToString()); + }); + ``` + + to `PreConfigureServices` to run authentication related unit tests. + +### UI Layer + +You can follow the migrations guides from IdentityServer to OpenIddict in **reverse order** to update your UIs. You can also check the source-code for [Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml) and [Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml.cs) files for **AuthServer** project. + +- [Angular UI Migration](OpenIddict-Angular.md) +- [MVC/Razor UI Migration](OpenIddict-Mvc.md) +- [Blazor-Server UI Migration](OpenIddict-Blazor-Server.md) +- [Blazor-Wasm UI Migration](OpenIddict-Blazor.md) + +## Source code of samples and module + +* [Open source tiered & separate auth server application migrate OpenIddict to Identity Server](https://github.com/abpframework/abp-samples/tree/master/OpenId2Ids) +* [IdentityServer module document](https://docs.abp.io/en/abp/6.0/Modules/IdentityServer) +* [IdentityServer module source code](https://github.com/abpframework/abp/tree/rel-6.0/modules/identityserver) diff --git a/docs/en/Migration-Guides/Index.md b/docs/en/Migration-Guides/Index.md index 4e6fc6b114..8a693970b2 100644 --- a/docs/en/Migration-Guides/Index.md +++ b/docs/en/Migration-Guides/Index.md @@ -2,6 +2,7 @@ The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../Upgrading.md). +- [7.0 to 7.1](Abp-7_1.md) - [6.0 to 7.0](Abp-7_0.md) - [5.3 to 6.0](Abp-6_0.md) - [5.2 to 5.3](Abp-5_3.md) diff --git a/docs/en/Migration-Guides/OpenIddict-Angular.md b/docs/en/Migration-Guides/OpenIddict-Angular.md index c3ca6a7fbb..fe0b3986ac 100644 --- a/docs/en/Migration-Guides/OpenIddict-Angular.md +++ b/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**: diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md b/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md index 65011f6124..93c9562a4d 100644 --- a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md +++ b/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**: diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor.md b/docs/en/Migration-Guides/OpenIddict-Blazor.md index 4a3e166cb9..09e137a0a6 100644 --- a/docs/en/Migration-Guides/OpenIddict-Blazor.md +++ b/docs/en/Migration-Guides/OpenIddict-Blazor.md @@ -62,7 +62,7 @@ typeof(AbpAccountWebOpenIddictModule), ``` -- In the **MyApplicationBlazorModule.cs** add `PreConfigureServices` like below with your application name as the audience: +- In the **MyApplicationHostModule.cs** add `PreConfigureServices` like below with your application name as the audience: ```csharp public override void PreConfigureServices(ServiceConfigurationContext context) @@ -79,7 +79,7 @@ } ``` -- In the **MyApplicationBlazorModule.cs** `ConfigureServices` method, **replace the method call**: +- In the **MyApplicationHostModule.cs** `ConfigureServices` method, **replace the method call**: From `ConfigureAuthentication(context, configuration);` to `ConfigureAuthentication(context);` and update the method as: @@ -90,7 +90,7 @@ } ``` -- In the **MyApplicationBlazorModule.cs** `OnApplicationInitialization` method, **replace the midware**: +- In the **MyApplicationHostModule.cs** `OnApplicationInitialization` method, **replace the midware**: ```csharp app.UseJwtTokenMiddleware(); @@ -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**: diff --git a/docs/en/Migration-Guides/OpenIddict-Mvc.md b/docs/en/Migration-Guides/OpenIddict-Mvc.md index 7fd411155d..8dd5ec6c94 100644 --- a/docs/en/Migration-Guides/OpenIddict-Mvc.md +++ b/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**: diff --git a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md b/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md index 2638895843..8aff29c84d 100644 --- a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md +++ b/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. diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md index dd01f81ff4..dfce0bf4b2 100644 --- a/docs/en/Module-Development-Basics.md +++ b/docs/en/Module-Development-Basics.md @@ -20,7 +20,6 @@ public class BlogModule : AbpModule { } - ```` ### Configuring Dependency Injection & Other Modules diff --git a/docs/en/Module-Entity-Extensions.md b/docs/en/Module-Entity-Extensions.md index 1a3c318c6b..47436a7dd9 100644 --- a/docs/en/Module-Entity-Extensions.md +++ b/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` diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md index 7beb5b128d..2ae77d532d 100644 --- a/docs/en/Modules/Account.md +++ b/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 diff --git a/docs/en/Modules/Audit-Logging.md b/docs/en/Modules/Audit-Logging.md index 30e1ef4e04..93a50c8387 100644 --- a/docs/en/Modules/Audit-Logging.md +++ b/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 diff --git a/docs/en/Modules/Background-Jobs.md b/docs/en/Modules/Background-Jobs.md index 6cce8a6c95..5fd57d812c 100644 --- a/docs/en/Modules/Background-Jobs.md +++ b/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 diff --git a/docs/en/Modules/Cms-Kit/Blogging.md b/docs/en/Modules/Cms-Kit/Blogging.md index 12039b1669..91c14d79e8 100644 --- a/docs/en/Modules/Cms-Kit/Blogging.md +++ b/docs/en/Modules/Cms-Kit/Blogging.md @@ -2,6 +2,12 @@ The blogging feature provides the necessary UI to manage and render blogs and blog posts. +## Enabling the Blogging Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## User Interface ### Menu Items diff --git a/docs/en/Modules/Cms-Kit/Comments.md b/docs/en/Modules/Cms-Kit/Comments.md index e5a5da592e..26aa7c0398 100644 --- a/docs/en/Modules/Cms-Kit/Comments.md +++ b/docs/en/Modules/Cms-Kit/Comments.md @@ -1,10 +1,16 @@ # 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. + +## Enabling the Comment Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. ## 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,14 +18,16 @@ The comment system provides a mechanism to group comment definitions by entity t Configure(options => { options.EntityTypes.Add(new CommentEntityTypeDefinition("Product")); + options.IsRecaptchaEnabled = true; //false by default }); ``` -> If you're using the blog feature, the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. +> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. `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 +41,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 +86,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 +102,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 diff --git a/docs/en/Modules/Cms-Kit/Global-Resources.md b/docs/en/Modules/Cms-Kit/Global-Resources.md index 7f639ad884..5e0e0154a0 100644 --- a/docs/en/Modules/Cms-Kit/Global-Resources.md +++ b/docs/en/Modules/Cms-Kit/Global-Resources.md @@ -2,6 +2,12 @@ CMS Kit Global Resources system allows to add global styles and scripts dynamically. +## Enabling the Global Resources Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## The User Interface ### Menu items diff --git a/docs/en/Modules/Cms-Kit/Index.md b/docs/en/Modules/Cms-Kit/Index.md index 5b7f3b5366..e40f78922e 100644 --- a/docs/en/Modules/Cms-Kit/Index.md +++ b/docs/en/Modules/Cms-Kit/Index.md @@ -16,11 +16,12 @@ The following features are currently available: * Provides a [**global resources**](Global-Resources.md) system to add global styles and scripts dynamically. * Provides a [**Dynamic Widget**](Dynamic-Widget.md) system to create dynamic widgets for page and blog posts. -Click to a feature to understand and learn how to use it. +> You can click on the any feature links above to understand and learn how to use it. -All features are individually usable. If you disable a feature, it completely disappears from your application, even from the database tables, by the help of the [Global Features](../../Global-Features.md) system. +All features are individually usable. If you disable a feature, it completely disappears from your application, even from the database tables, with the help of the [Global Features](../../Global-Features.md) system. ## Pre Requirements + - This module depends on [BlobStoring](../../Blob-Storing.md) module for keeping media content. > Make sure `BlobStoring` module is installed and at least one provider is configured properly. For more information, check the [documentation](../../Blob-Storing.md). @@ -62,7 +63,7 @@ GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit => This module follows the [module development best practices guide](https://docs.abp.io/en/abp/latest/Best-Practices/Index) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. -CMS kit packages are designed for various usage scenarios. If you check the [CMS kit packages](https://www.nuget.org/packages?q=Volo.CmsKit), you will see that some packages have `Admin` and `Public` suffixes. The reason is that the module has two application layers, considering they might be used in different type of applications. These application layers uses a single domain layer. +CMS kit packages are designed for various usage scenarios. If you check the [CMS kit packages](https://www.nuget.org/packages?q=Volo.CmsKit), you will see that some packages have `Admin` and `Public` suffixes. The reason is that the module has two application layers, considering they might be used in different type of applications. These application layers uses a single domain layer: - `Volo.CmsKit.Admin.*` packages contain the functionalities required by admin (back office) applications. - `Volo.CmsKit.Public.*` packages contain the functionalities used in public websites where users read blog posts or leave comments. diff --git a/docs/en/Modules/Cms-Kit/Menus.md b/docs/en/Modules/Cms-Kit/Menus.md index 87486326da..b469c322ab 100644 --- a/docs/en/Modules/Cms-Kit/Menus.md +++ b/docs/en/Modules/Cms-Kit/Menus.md @@ -1,7 +1,13 @@ -# CMS Kit: Pages +# CMS Kit: Menus CMS Kit Menu system allows to manage public menus dynamically. +## Enabling the Menu Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## The User Interface ### Menu items @@ -20,22 +26,18 @@ Menus page is used to manage dynamic public menus in the system. ![cms-kit-menus-page](../../images/cmskit-module-menus-page.png) - - -Created menus will be visible on public site. +The created menu items will be visible on the public-web side, as shown below: ![cms-kit-public-menus](../../images//cmskit-module-menus-public.png) -# Internals +## Internals -## Domain Layer +### Domain Layer #### Aggregates This module follows the [Entity Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Entities) guide. -##### Menus - - `MenuItem` (aggregate root): A Menu Item presents a single node at menu tree. #### Repositories diff --git a/docs/en/Modules/Cms-Kit/Pages.md b/docs/en/Modules/Cms-Kit/Pages.md index 7945bfd5ff..cc1f5074f4 100644 --- a/docs/en/Modules/Cms-Kit/Pages.md +++ b/docs/en/Modules/Cms-Kit/Pages.md @@ -2,6 +2,12 @@ CMS Kit Page system allows you to create dynamic pages by specifying URLs, which is the fundamental feature of a CMS. +## Enabling the Pages Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## The User Interface ### Menu items @@ -24,5 +30,5 @@ You can create or edit an existing page on this page. ![pages-edit](../../images/cmskit-module-pages-edit.png) -When you create a page, you can access the created page via `/pages/{slug}` URL. +When you create a page, you can access the created page via `/{slug}` URL. diff --git a/docs/en/Modules/Cms-Kit/Ratings.md b/docs/en/Modules/Cms-Kit/Ratings.md index 8702feb24e..db58602c15 100644 --- a/docs/en/Modules/Cms-Kit/Ratings.md +++ b/docs/en/Modules/Cms-Kit/Ratings.md @@ -4,6 +4,12 @@ CMS kit provides a **rating** system to to add ratings feature to any kind of re ![ratings](../../images/cmskit-module-ratings.png) +## Enabling the Rating Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## Options The rating system provides a mechanism to group ratings by entity types. For example, if you want to use the rating system for products, you need to define an entity type named `Product` and then add ratings under the defined entity type. @@ -17,7 +23,7 @@ Configure(options => }); ``` -> If you're using the blog feature, the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. +> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. You can easily override or remove the predefined entity types in `Configure` method like shown above. `CmsKitRatingOptions` properties: diff --git a/docs/en/Modules/Cms-Kit/Reactions.md b/docs/en/Modules/Cms-Kit/Reactions.md index 86da9ccb72..457d5e0f26 100644 --- a/docs/en/Modules/Cms-Kit/Reactions.md +++ b/docs/en/Modules/Cms-Kit/Reactions.md @@ -8,6 +8,12 @@ Reaction component allows users to react to your content via pre-defined icons/e You can also customize the reaction icons shown in the reaction component. +## Enabling the Reaction Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## Options Reaction system provides a mechanism to group reactions by entity types. For example, if you want to use the reaction system for products, you need to define an entity type named `Product`, and then add reactions under the defined entity type. @@ -32,7 +38,7 @@ Configure(options => }); ``` -> If you're using the comment or blog features, the ABP framework defines predefined reactions for these features automatically. +> If you're using the [Comment](Comments.md) or [Blogging](Blogging.md) features, the ABP framework defines predefined reactions for these features automatically. `CmsKitReactionOptions` properties: diff --git a/docs/en/Modules/Cms-Kit/Tags.md b/docs/en/Modules/Cms-Kit/Tags.md index 17bc5cf47f..8f91bc67c7 100644 --- a/docs/en/Modules/Cms-Kit/Tags.md +++ b/docs/en/Modules/Cms-Kit/Tags.md @@ -2,6 +2,12 @@ CMS kit provides a **tag** system to tag any kind of resources, like a blog post. +## Enabling the Tag Management Feature + +By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../Global-Features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](https://docs.abp.io/en/abp/latest/Features) to disable a CMS Kit feature on runtime. + +> Check the ["How to Install" section of the CMS Kit Module documentation](Index.md#how-to-install) to see how to enable/disable CMS Kit features on development time. + ## Options The tag system provides a mechanism to group tags by entity types. For example, if you want to use the tag system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product` and add tags under these entity types. @@ -17,7 +23,7 @@ Configure(options => }); ``` -> If you're using the blog feature, the ABP framework defines an entity type for the blog feature automatically. +> If you're using the [Blogging Feature](Blogging.md), the ABP framework defines an entity type for the blog feature automatically. `CmsKitTagOptions` properties: diff --git a/docs/en/Modules/Database-Tables.md b/docs/en/Modules/Database-Tables.md new file mode 100644 index 0000000000..8d0290b311 --- /dev/null +++ b/docs/en/Modules/Database-Tables.md @@ -0,0 +1,575 @@ +# Database Tables + +This documentation describes all database tables and their purposes. You can read this documentation to get general knowledge of the database tables that come from each module. + +## [Audit Logging Module](Audit-Logging.md) + +### AbpAuditLogs + +This table stores information about the audit logs in the application. Each record represents an audit log and tracks the actions performed in the application. + +### AbpAuditLogActions + +This table stores information about the actions performed in the application, which are logged for auditing purposes. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpAuditLogs](#abpauditlogs) | Id | Links each action to a specific audit log. | + +### AbpEntityChanges + +This table stores information about entity changes in the application, which are logged for auditing purposes. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpAuditLogs](#abpauditlogs) | Id | Links each entity change to a specific audit log. | + +### AbpEntityPropertyChanges + +This table stores information about property changes to entities in the application, which are logged for auditing purposes. + +## Uses + +| Table | Column | Description | +| --- | --- | --- | +| [AbpEntityChanges](#abpentitychanges) | Id | Links each property change to a specific entity change. | + +## [Background Jobs Module](Background-Jobs.md) + +### AbpBackgroundJobs + +This table stores information about the background jobs in the application and facilitates their efficient management and tracking. Each entry in the table contains details of a background job, including the job name, arguments, try count, next try time, last try time, abandoned status, and priority. + +## [Tenant Management Module](Tenant-Management.md) + +### AbpTenants + +This table stores information about the tenants. Each record represents a tenant and contains information about the tenant, such as name and other details. + +### AbpTenantConnectionStrings + +This table stores information about the tenant database connection strings. When you define a connection string for a tenant, a new record will be added to this table. You can query this database to get connection strings by tenants. + +## Uses + +| Table | Column | Description | +| --- | --- | --- | +| [AbpTenants](#abptenants) | Id | The `Id` column in the `AbpTenants` table is used to associate the tenant connection string with the corresponding tenant. | + +## Blogging Module + +### BlgUsers + +This table stores information about the blog users. When a new identity user is created, a new record will be added to this table. + +### BlgBlogs + +This table serves to store blog information and semantically separates the posts of each blog. + +### BlgPosts + +This table stores information about the blog posts. You can query this table to get blog posts by blogs. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [BlgBlogs](#blgblogs) | Id | To associate the blog post with the corresponding blog. | +### BlgComments + +This table stores information about the comments made on blog posts. You can query this table to get comments by posts. +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [BlgPosts](#blgposts) | Id | Links the comment to the corresponding blog post. | +| [BlgComments](#blgcomments) | Id | Links the comment to the parent comment. | + +### BlgTags + +This table stores information about the tags. When a new tag is used, a new record will be added to this table. You can query this table to get tags by blogs. + +### BlgPostTags + +This table is used to associate tags with blog posts in order to categorize and organize the content. You can query this table to get post tags by posts. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [BlgTags](#blgtags) | Id | Links the post tag to the corresponding tag. | +| [BlgPosts](#blgposts) | Id | Links the post tag to the corresponding blog post. | + +## [CMS Kit Module](Cms-Kit/Index.md) + +### CmsUsers + +This table stores information about the CMS Kit module users. When a new identity user is created, a new record will be added to this table. + +### CmsBlogs + +This table serves to store blog information and semantically separates the posts of each blog. + +### CmsBlogPosts + +This table stores information about the blog posts. You can query this table to get blog posts by blogs. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [CmsUsers](#cmsusers) | Id | Links the blog post to the corresponding author. | + +### CmsBlogFeatures + +This table stores information about the blog features. You can query this table to get blog features by blogs. + +### CmsComments + +This table is utilized by the [CMS Kit Comment system](Cms-Kit/Comments.md) to store comments made on the blog posts. You can query this table to get comments by posts. + +### CmsTags + +This table stores information about the tags. When a new tag is used, a new record will be added to this table. You can query this table to get tags by blogs. + +### CmsEntityTags + +This table is utilized by the [Tag Management system](Cms-Kit/Tags.md) to store tags and their relationship with various entities, thus enabling efficient categorization and organization of content. You can query this table to get entity tags by entities. + +### CmsGlobalResources + +This table is a database table for the [CMS Kit Global Resources system](Cms-Kit/Global-Resources.md), allowing dynamic addition of global styles and scripts. + +### CmsMediaDescriptors + +This table is utilized by the CMS kit module to manage media files by using the [BlobStoring](../Blob-Storing.md) module. + +### CmsMenuItems + +This table is used by the [CMS Kit Menu system](Cms-Kit/Menus.md) to manage and store information about dynamic public menus, including details such as menu item display names, URLs, and hierarchical relationships. + +### CmsPages + +This table is utilized by the [CMS Kit Page system](Cms-Kit/Pages.md) to store dynamic pages within the application, including information such as page URLs, titles, and content. + +### CmsRatings + +This table is utilized by the [CMS Kit Rating system](Cms-Kit/Ratings.md) to store ratings made on blog posts. You can query this table to get ratings by posts. + +### CmsUserReactions + +This table is utilized by the [CMS Kit Reaction system](Cms-Kit/Reactions.md) to store reactions made on blog posts. You can query this table to get reactions by posts. + +## [Docs Module](Docs.md) + +### DocsProjects + +This table stores project information to categorize documents according to different projects. + +### DocsDocuments + +This table retrieves the document if it's not found in the cache. The documentation is being updated when the content is retrieved from the database. + +### DocsDocumentContributors + +This table stores information about the contributors of the documents. You can query this table to get document contributors by documents. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [DocsDocuments](#docsdocuments) | Id | Links the document contributor to the corresponding document. | + +## [Feature Management Module](Feature-Management.md) + +### AbpFeatureGroups + +This table stores information about the feature groups in the application. For example, you can group all the features in the [`AbpFeatures`](#abpfeatures) table related to the `Identity` module under the `Identity` group. + +### AbpFeatures + +This table stores information about the features in the application. You can use the `Name` column to link each feature with its corresponding feature value in the [`AbpFeatureValues`](#abpfeaturevalues) table, so that you can easily manage and organize the features. + +### AbpFeatureValues + +This table stores the values of the features for different providers. You can use the `Name` column to link each feature value with its corresponding feature in the [`AbpFeatures`](#abpfeatures) table, so that you can easily manage and organize the features. + +## [Identity Module](Identity.md) + +### AbpUsers + +This table stores information about the identity users in the application. + +### AbpRoles + +This table stores information about the roles in the application. Roles are used to manage and control access to different parts of the application by assigning permissions and claims to roles and then assigning those roles to users. This table is important for managing and organizing the roles in the application, and for defining the access rights of the users. + +### AbpClaimTypes + +This table stores information about the claim types used in the application. You can use the `Name`, `Regex` columns to filter the claim types by name, and regex pattern respectively, so that you can easily manage and track the claim types in the application. + +### AbpLinkUsers + +This table is useful for linking multiple user accounts across different tenants or applications to a single user, allowing them to easily switch between their accounts. + +### AbpUserClaims + +This table can manage user-based access control by allowing to assign claims to users, which describes the access rights of the individual user. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpUsers](#abpusers) | Id | Links the user claim to the corresponding user. | + +### AbpUserLogins + +This table can store information about the user's external logins such as login with Facebook, Google, etc. and it can also be used to track the login history of users. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpUsers](#abpusers) | Id | Links the user login to the corresponding user. | + +### AbpUserRoles + +This table can manage user-based access control by allowing to assign roles to users, which describe the access rights of the individual user. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpUsers](#abpusers) | Id | Links the user role to the corresponding user. | +| [AbpRoles](#abproles) | Id | Links the user role to the corresponding role. | + +### AbpUserTokens + +This table can store information about user's refresh tokens, access tokens and other tokens used in the application. It can also be used to invalidate or revoke user tokens. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpUsers](#abpusers) | Id | Links the user token to the corresponding user. | + +### AbpOrganizationUnits + +This table is useful for creating and managing a hierarchical structure of the organization, allowing to group users and assign roles based on the organization structure. You can use the `Code`, `ParentId` columns to filter the organization units by code and parent id respectively, so that you can easily manage and track the organization units in the application. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpOrganizationUnits](#abporganizationunits) | ParentId | Links the organization unit to its parent organization unit. | + +### AbpOrganizationUnitRoles + +This table is useful for managing role-based access control at the level of organization units, allowing to assign different roles to different parts of the organization structure. You can use the `OrganizationUnitId`, `RoleId` columns to filter the roles by organization unit id and role id respectively, so that you can easily manage and track the roles assigned to organization units in the application. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpOrganizationUnits](#abporganizationunits) | Id | Links the organization unit role to the corresponding organization unit. | +| [AbpRoles](#abproles) | Id | Links the organization unit role to the corresponding role. | + +### AbpUserOrganizationUnits + +This table stores information about the organization units assigned to the users in the application. This table can manage user-organization unit relationships, and to group users based on the organization structure. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpUsers](#abpusers) | Id | Links the user organization unit to the corresponding user. | +| [AbpOrganizationUnits](#abporganizationunits) | Id | Links the user organization unit to the corresponding organization unit. | + +### AbpRoleClaims + +This table is useful for managing role-based access control by allowing to assign claims to roles, which describes the access rights of the users that belong to that role. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpRoles](#abproles) | Id | Links the role claim to the corresponding role. | + +### AbpSecurityLogs + +This table logs important operations and changes related to user accounts, allowing users to save the security logs for future reference. + +## [Permission Management](Permission-Management.md) + +### AbpPermissionGroups + +This table is important for managing and organizing the permissions in the application, by grouping them into logical categories. + +### AbpPermissions + +This table is important for managing and controlling access to different parts of the application and for defining the granular permissions that make up the larger permissions or roles. + +### AbpPermissionGrants + +The table stores and manage the permissions in the application and to keep track of permissions that are granted, to whom and when. Columns such as `Name`, `ProviderName`, `ProviderKey`, `TenantId` can be used to filter the granted permissions by name, provider name, provider key, and tenant id respectively, so that you can easily manage and track the granted permissions in the application. + +## [Setting Management](Setting-Management.md) + +### AbpSettings + +This table stores key-value pairs of settings for the application, and it allows dynamic configuration of the application without the need for recompilation. + +## [OpenIddict](OpenIddict.md) + +### OpenIddictApplications + +This table can store information about the OpenID Connect applications, including the client id, client secret, redirect URI, and other relevant information. It can also be used to authenticate and authorize clients using OpenID Connect protocol. + +### OpenIddictAuthorizations + +This table stores the OpenID Connect authorization data in the application. It can also be used to manage and validate the authorization grants issued to clients and users. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [OpenIddictApplications](#openiddictapplications) | Id | Links the authorization to the corresponding application. | + +### OpenIddictTokens + +This table can store information about the OpenID Connect tokens, including the token payload, expiration, type, and other relevant information. It can also be used to manage and validate the tokens issued to clients and users, such as access tokens and refresh tokens, and to control access to protected resources. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [OpenIddictApplications](#openiddictapplications) | Id | Links the token to the corresponding application. | +| [OpenIddictAuthorizations](#openiddictauthorizations) | Id | Links the token to the corresponding authorization. | + +### OpenIddictScopes + +This table can store information about the OpenID Connect scopes, including the name and description of the scope. It can also be used to define the permissions or access rights associated with the scopes, which are then used to control access to protected resources. + +## [IdentityServer](IdentityServer.md) + +### IdentityServerApiResources + +This table can store information about the API resources, including the resource name, display name, description, and other relevant information. It can also be used to define the scopes, claims, and properties associated with the API resources, which are then used to control access to protected resources. + +### IdentityServerIdentityResources + +This table can store information about the identity resources, including the name, display name, description, and enabled status. + +### IdentityServerClients + +This table can store information about the clients, including the client id, client name, client URI and other relevant information. It can also be used to define the scopes, claims, and properties associated with the clients, which are then used to control access to protected resources. + +### IdentityServerApiScopes + +This table can store information about the API scopes, including the scope name, display name, description, and other relevant information. It can also be used to define the claims and properties associated with the API scopes, which are then used to control access to protected resources. + +### IdentityServerApiResourceClaims + +This table can store information about the claims of an API resource, including the claim type and API resource id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the claim to the corresponding API resource. | + +### IdentityServerIdentityResourceClaims + +This table can store information about the claims of an identity resource, including the claim type and identity resource id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerIdentityResources](#identityserveridentityresources) | Id | Links the claim to the corresponding identity resource. | + +### IdentityServerClientClaims + +This table can store information about the claims of a client, including the claim type, claim value and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the claim to the corresponding client. | + +### IdentityServerApiScopeClaims + +This table can store information about the claims of an API scope, including the claim type and API scope id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiScopes](#identityserverapiscopes) | Id | Links the claim to the corresponding API scope. | + +### IdentityServerApiResourceProperties + +This table can store information about properties, including the property key and value, and the associated API resource. These properties can store additional metadata or configuration information related to the API resources. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the property to the corresponding API resource. | + +### IdentityServerIdentityResourceProperties + +This table can store information about properties, including the property key and value, and the associated identity resource. These properties can store additional metadata or configuration information related to the identity resources. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerIdentityResources](#identityserveridentityresources) | Id | Links the property to the corresponding identity resource. | + +### IdentityServerClientProperties + +This table can be store information about the properties of a client, including the key, value and client id. These properties can store additional metadata or configuration information related to the clients. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the property to the corresponding client. | + +### IdentityServerApiScopeProperties + +This table can store information about the properties of an API scope, including the key, value and API scope id. These properties can store additional metadata or configuration information related to the API scopes. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiScopes](#identityserverapiscopes) | Id | Links the property to the corresponding API scope. | + +### IdentityServerApiResourceScopes + +This table can store information about the scopes of an API resource, including the scope name and API resource id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the scope to the corresponding API resource. | + +### IdentityServerClientScopes + + This table can store information about the scopes of a client, including the scope and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the scope to the corresponding client. | + +### IdentityServerApiResourceSecrets + +This table can store information about the secrets of an API resource, including the secret value, expiration date, and API resource id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerApiResources](#identityserverapiresources) | Id | Links the secret to the corresponding API resource. | + +### IdentityServerClientSecrets + +This table can store information about the secrets of a client, including the secret value, expiration date, and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the secret to the corresponding client. | + +### IdentityServerClientCorsOrigins + +This table can store information about the CORS origins of a client, including the origin and client id. It can also be used to manage and validate the CORS origins of a client. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the CORS origin to the corresponding client. | + +### IdentityServerClientGrantTypes + +This table can store information about the grant types of a client, including the grant type and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the grant type to the corresponding client. | + +### IdentityServerClientIdPRestrictions + +This table can store information about the identity provider restrictions of a client, including the identity provider and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the identity provider restriction to the corresponding client. | + +### IdentityServerClientPostLogoutRedirectUris + +This table can store information about the post logout redirect URIs of a client, including the post logout redirect URI and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the post logout redirect URI to the corresponding client. | + +### IdentityServerClientRedirectUris + +This table can store information about the redirect URIs of a client, including the redirect URI and client id. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [IdentityServerClients](#identityserverclients) | Id | Links the redirect URI to the corresponding client. | + +### IdentityServerDeviceFlowCodes + +This table can store information about the device flow codes, including the user code, device code, subject id, client id, creation time, expiration, data and session id. + +### IdentityServerPersistedGrants + +This table can store information about the persisted grants, including the key, type, subject id, client id, creation time, expiration, and data. + +## Others + +### AbpBlobContainers + +This table is important for providing a better user experience by allowing the application to support multiple containers and providing BLOB-specific features. + +### AbpBlobs + +This table stores the binary data of BLOBs (binary large objects) in the application. Each BLOB is related to a container in the [AbpBlobContainers](#abpblobcontainers) table, where the container name, tenant id and other properties of the container can be found. + +#### Foreign Keys + +| Table | Column | Description | +| --- | --- | --- | +| [AbpBlobContainers](#abpblobcontainers) | Id | Links the BLOB to the corresponding container. | + +### AbpLocalizationResources + +This table stores the localization resources for the application. This table is important for providing a better user experience by allowing the application to support multiple resources and providing localized text and other localization-specific features. + +### AbpLocalizationTexts + +The table contains the resource name, culture name, and a json encoded value which holds the key-value pair of localization text. It allows for efficient storage and management of localization texts and allows for easy update or addition of new translations for specific resources and cultures. diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index 4777275cd6..143ec7168d 100644 --- a/docs/en/Modules/Docs.md +++ b/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(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" diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md index 9677e2f470..d6cf470391 100644 --- a/docs/en/Modules/Feature-Management.md +++ b/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 diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md index 4cd8bef316..f73e8073b2 100644 --- a/docs/en/Modules/Identity.md +++ b/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 diff --git a/docs/en/Modules/IdentityServer.md b/docs/en/Modules/IdentityServer.md index 90e9fe19eb..90c08f6c6b 100644 --- a/docs/en/Modules/IdentityServer.md +++ b/docs/en/Modules/IdentityServer.md @@ -1,10 +1,12 @@ # IdentityServer Module -IdentityServer module provides a full integration with the [IdentityServer](https://github.com/IdentityServer/IdentityServer4) (IDS) framework, which provides advanced authentication features like single sign-on and API access control. This module persists clients, resources and other IDS-related objects to database. +IdentityServer module provides a full integration with the [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) (IDS) framework, which provides advanced authentication features like single sign-on and API access control. This module persists clients, resources and other IDS-related objects to database. **This module is replaced by** [OpenIddict module](https://docs.abp.io/en/abp/latest/Modules/OpenIddict) after ABP v6.0 in the startup templates. + +> Note: You can not use IdentityServer and OpenIddict modules together. They are separate OpenID provider libraries for the same job. ## 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. +You don't need this module when you are using OpenIddict module. However, if you want to keep using IdentityServer4 for your applications, you can install this module and remove the OpenIddict module. 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 diff --git a/docs/en/Modules/OpenIddict.md b/docs/en/Modules/OpenIddict.md index 3a015011e6..cfa35b7e2c 100644 --- a/docs/en/Modules/OpenIddict.md +++ b/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(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(options => +Configure(options => { //Set options here... }); diff --git a/docs/en/Modules/Permission-Management.md b/docs/en/Modules/Permission-Management.md index b97ddfca6c..e77a284a42 100644 --- a/docs/en/Modules/Permission-Management.md +++ b/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 diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md index d99c40da43..bfdac23104 100644 --- a/docs/en/MongoDB.md +++ b/docs/en/MongoDB.md @@ -393,7 +393,7 @@ One advantage of using interface for a MongoDbContext is then it becomes replace Once you properly define and use an interface for a MongoDbContext , then any other implementation can use the following ways to replace it: -**ReplaceDbContextAttribute** +#### ReplaceDbContext Attribute ```csharp [ReplaceDbContext(typeof(IBookStoreMongoDbContext))] @@ -403,7 +403,7 @@ public class OtherMongoDbContext : AbpMongoDbContext, IBookStoreMongoDbContext } ``` -**ReplaceDbContext option** +#### ReplaceDbContext Option ```csharp context.Services.AddMongoDbContext(options => @@ -415,6 +415,22 @@ context.Services.AddMongoDbContext(options => In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime. +#### Replacing with Multi-Tenancy + +It is also possible to replace a DbContext based on the [multi-tenancy](Multi-Tenancy.md) side. `ReplaceDbContext` attribute and `ReplaceDbContext` method can get a `MultiTenancySides` option with a default value of `MultiTenancySides.Both`. + +**Example:** Replace DbContext only for tenants, using the `ReplaceDbContext` attribute + +````csharp +[ReplaceDbContext(typeof(IBookStoreDbContext), MultiTenancySides.Tenant)] +```` + +**Example:** Replace DbContext only for the host side, using the `ReplaceDbContext` method + +````csharp +options.ReplaceDbContext(MultiTenancySides.Host); +```` + ### Customize Bulk Operations If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IMongoDbBulkOperationProvider`. diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index a1a5ac5cce..23ed2371dd 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -164,6 +164,14 @@ If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method > See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. +### Delete Direct + +`DeleteDirectAsync` method of the repository deletes all entities those fit to the given predicate. It directly deletes entities from database, without fetching them. + +Some features (like soft-delete, multi-tenancy and audit logging) won't work, so use this method carefully when you need it. Use the `DeleteAsync` method if you need to these features. + +> Currently only [EF Core supports it](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#basic-executedelete-examples), For the ORMs doesn't support direct delete, we will fallback to the existing `DeleteAsync` method. + ### Ensure Entities Exists The `EnsureExistsAsync` extension method accepts entity id or entities query expression to ensure entities exist, otherwise, it will throw `EntityNotFoundException`. diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md index 45f33dbd9c..7cf8948515 100644 --- a/docs/en/Road-Map.md +++ b/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. diff --git a/docs/en/Startup-Templates/Application-Single-Layer.md b/docs/en/Startup-Templates/Application-Single-Layer.md index 95ebad55a7..02a743c5ee 100644 --- a/docs/en/Startup-Templates/Application-Single-Layer.md +++ b/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: @@ -32,19 +32,18 @@ abp new Acme.BookStore -t app-nolayers This template provides multiple UI frameworks: * `mvc`: ASP.NET Core MVC UI with Razor Pages (default) +* `blazor`: Blazor UI * `blazor-server`: Blazor Server UI * `angular`: Angular UI * `none`: Without UI (for HTTP API development) -> This template doesn't have Blazor WebAssembly UI, because it requires 3 projects at least (server-side, UI and shared library between these two projects). We are recommending to use the layered [application startup template](Application.md) for Blazor WebAssembly projects. - Use the `-u` (or `--ui`) option to specify the UI framework while creating the solution: ```bash abp new Acme.BookStore -t app-nolayers -u angular ``` -This example specifies the UI type (the `-u` option) as `angular`. You can also specify `mvc`, `blazor-server` or `none` for the UI type. +This example specifies the UI type (the `-u` option) as `angular`. You can also specify `mvc`, `blazor`, `blazor-server` or `none` for the UI type. ### Specify the Database Provider diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 6a47363e8c..98ccdd10d2 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/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: diff --git a/docs/en/Startup-Templates/Module.md b/docs/en/Startup-Templates/Module.md index afb6496ed5..faf4cb41a2 100644 --- a/docs/en/Startup-Templates/Module.md +++ b/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: diff --git a/docs/en/Themes/LeptonXLite/Angular.md b/docs/en/Themes/LeptonXLite/Angular.md index 950cd20c8b..56b003e606 100644 --- a/docs/en/Themes/LeptonXLite/Angular.md +++ b/docs/en/Themes/LeptonXLite/Angular.md @@ -14,13 +14,19 @@ To add `LeptonX-lite` into your project, - Install `@abp/ng.theme.lepton-x` -`yarn add @abp/ng.theme.lepton-x@preview` +```bash +yarn add @abp/ng.theme.lepton-x +``` - Install `bootstrap-icons` -`yarn add bootstrap-icons` +```bash +yarn add bootstrap-icons +``` + +- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one in the following link : -- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one. +* [Styles - Angular UI](../../UI/Angular/Theme-Configurations.md) Add the following style diff --git a/docs/en/Themes/LeptonXLite/AspNetCore.md b/docs/en/Themes/LeptonXLite/AspNetCore.md index f480141120..b134135a04 100644 --- a/docs/en/Themes/LeptonXLite/AspNetCore.md +++ b/docs/en/Themes/LeptonXLite/AspNetCore.md @@ -38,7 +38,7 @@ Configure(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"); diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index eb00a94e0c..0b50992274 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/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 { - public class Book : AuditedAggregateRoot - { - 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, + IIdentityDbContext, + ITenantManagementDbContext { - public class BookStoreDbContext : - AbpDbContext, - 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(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(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 _bookRepository; + private readonly IRepository _bookRepository; - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } + public BookStoreDataSeederContributor(IRepository 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 { - public class BookDto : AuditedEntityDto - { - 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(); - } + CreateMap(); } } ```` @@ -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(); - CreateMap(); - } + CreateMap(); + CreateMap(); } } ```` @@ -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 repository) + : base(repository) { - public BookAppService(IRepository repository) - : base(repository) - { - } } } ```` diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md index 6418db7e71..6178ebbb8e 100644 --- a/docs/en/Tutorials/Part-10.md +++ b/docs/en/Tutorials/Part-10.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 @@ -55,7 +52,7 @@ public Guid AuthorId { get; set; } {{if DB=="EF"}} -> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will done below) which makes your application code simpler. +> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will be doing below) which makes your application code simpler. {{end}} @@ -105,6 +102,7 @@ This should create a new migration class with the following code in its `Up` met migrationBuilder.AddColumn( name: "AuthorId", table: "AppBooks", + type: "uniqueidentifier", nullable: false, defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); @@ -143,72 +141,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 _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _bookRepository = bookRepository; + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + public async Task SeedAsync(DataSeedContext context) { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) + if (await _bookRepository.GetCountAsync() > 0) { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; + return; } - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) + var orwell = 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)." + ) + ); + + var douglas = 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 _bookRepository.InsertAsync( + new Book { - return; - } - - var orwell = 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)." - ) - ); - - var douglas = 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 _bookRepository.InsertAsync( - new Book - { - AuthorId = orwell.Id, // SET THE AUTHOR - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = douglas.Id, // SET THE AUTHOR - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } + AuthorId = orwell.Id, // SET THE AUTHOR + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + AuthorId = douglas.Id, // SET THE AUTHOR + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } } ```` @@ -250,22 +247,21 @@ The final `BookDto` class should be following: using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class BookDto : AuditedEntityDto { - public class BookDto : AuditedEntityDto - { - public Guid AuthorId { get; set; } + public Guid AuthorId { get; set; } - public string AuthorName { get; set; } + public string AuthorName { get; set; } - 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; } } ``` @@ -285,12 +281,11 @@ Create a new class, `AuthorLookupDto`, inside the `Books` folder of the `Acme.Bo using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class AuthorLookupDto : EntityDto { - public class AuthorLookupDto : EntityDto - { - public string Name { get; set; } - } + public string Name { get; set; } } ```` @@ -306,18 +301,17 @@ using System.Threading.Tasks; 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 - { - // ADD the NEW METHOD - Task> GetAuthorLookupAsync(); - } + // ADD the NEW METHOD + Task> GetAuthorLookupAsync(); } ```` @@ -343,119 +337,118 @@ using Volo.Abp.Application.Services; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +[Authorize(BookStorePermissions.Books.Default)] +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 { - [Authorize(BookStorePermissions.Books.Default)] - 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 + private readonly IAuthorRepository _authorRepository; + + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) { - private readonly IAuthorRepository _authorRepository; + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) + public override async Task GetAsync(Guid id) + { + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); + + //Prepare a query to join books and authors + var query = from book in queryable + join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id + where book.Id == id + select new { book, author }; + + //Execute the query and get the book with author + var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); + if (queryResult == null) { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; + throw new EntityNotFoundException(typeof(Book), id); } - public override async Task GetAsync(Guid id) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - where book.Id == id - select new { book, author }; - - //Execute the query and get the book with author - var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); - if (queryResult == null) - { - throw new EntityNotFoundException(typeof(Book), id); - } + var bookDto = ObjectMapper.Map(queryResult.book); + bookDto.AuthorName = queryResult.author.Name; + return bookDto; + } - var bookDto = ObjectMapper.Map(queryResult.book); - bookDto.AuthorName = queryResult.author.Name; - return bookDto; - } + public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) + { + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); - public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); + //Prepare a query to join books and authors + var query = from book in queryable + join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id + select new {book, author}; - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - select new {book, author}; + //Paging + query = query + .OrderBy(NormalizeSorting(input.Sorting)) + .Skip(input.SkipCount) + .Take(input.MaxResultCount); - //Paging - query = query - .OrderBy(NormalizeSorting(input.Sorting)) - .Skip(input.SkipCount) - .Take(input.MaxResultCount); + //Execute the query and get a list + var queryResult = await AsyncExecuter.ToListAsync(query); - //Execute the query and get a list - var queryResult = await AsyncExecuter.ToListAsync(query); + //Convert the query result to a list of BookDto objects + var bookDtos = queryResult.Select(x => + { + var bookDto = ObjectMapper.Map(x.book); + bookDto.AuthorName = x.author.Name; + return bookDto; + }).ToList(); - //Convert the query result to a list of BookDto objects - var bookDtos = queryResult.Select(x => - { - var bookDto = ObjectMapper.Map(x.book); - bookDto.AuthorName = x.author.Name; - return bookDto; - }).ToList(); + //Get the total count with another query + var totalCount = await Repository.GetCountAsync(); - //Get the total count with another query - var totalCount = await Repository.GetCountAsync(); + return new PagedResultDto( + totalCount, + bookDtos + ); + } - return new PagedResultDto( - totalCount, - bookDtos - ); - } + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); + private static string NormalizeSorting(string sorting) + { + if (sorting.IsNullOrEmpty()) + { + return $"book.{nameof(Book.Name)}"; } - private static string NormalizeSorting(string sorting) + if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) { - if (sorting.IsNullOrEmpty()) - { - return $"book.{nameof(Book.Name)}"; - } - - if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) - { - return sorting.Replace( - "authorName", - "author.Name", - StringComparison.OrdinalIgnoreCase - ); - } - - return $"book.{sorting}"; + return sorting.Replace( + "authorName", + "author.Name", + StringComparison.OrdinalIgnoreCase + ); } + + return $"book.{sorting}"; } } ``` @@ -487,108 +480,107 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +[Authorize(BookStorePermissions.Books.Default)] +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 { - [Authorize(BookStorePermissions.Books.Default)] - 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 - { - private readonly IAuthorRepository _authorRepository; + private readonly IAuthorRepository _authorRepository; - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; - } + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) + { + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } - public async override Task GetAsync(Guid id) - { - var book = await Repository.GetAsync(id); - var bookDto = ObjectMapper.Map(book); + public async override Task GetAsync(Guid id) + { + var book = await Repository.GetAsync(id); + var bookDto = ObjectMapper.Map(book); - var author = await _authorRepository.GetAsync(book.AuthorId); - bookDto.AuthorName = author.Name; + var author = await _authorRepository.GetAsync(book.AuthorId); + bookDto.AuthorName = author.Name; - return bookDto; - } + return bookDto; + } - public async override Task> - GetListAsync(PagedAndSortedResultRequestDto input) + public async override Task> + GetListAsync(PagedAndSortedResultRequestDto input) + { + //Set a default sorting, if not provided + if (input.Sorting.IsNullOrWhiteSpace()) { - //Set a default sorting, if not provided - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Book.Name); - } + input.Sorting = nameof(Book.Name); + } - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); - //Get the books - var books = await AsyncExecuter.ToListAsync( - queryable - .OrderBy(input.Sorting) - .Skip(input.SkipCount) - .Take(input.MaxResultCount) - ); + //Get the books + var books = await AsyncExecuter.ToListAsync( + queryable + .OrderBy(input.Sorting) + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + ); - //Convert to DTOs - var bookDtos = ObjectMapper.Map, List>(books); + //Convert to DTOs + var bookDtos = ObjectMapper.Map, List>(books); - //Get a lookup dictionary for the related authors - var authorDictionary = await GetAuthorDictionaryAsync(books); + //Get a lookup dictionary for the related authors + var authorDictionary = await GetAuthorDictionaryAsync(books); - //Set AuthorName for the DTOs - bookDtos.ForEach(bookDto => bookDto.AuthorName = - authorDictionary[bookDto.AuthorId].Name); + //Set AuthorName for the DTOs + bookDtos.ForEach(bookDto => bookDto.AuthorName = + authorDictionary[bookDto.AuthorId].Name); - //Get the total count with another query (required for the paging) - var totalCount = await Repository.GetCountAsync(); + //Get the total count with another query (required for the paging) + var totalCount = await Repository.GetCountAsync(); - return new PagedResultDto( - totalCount, - bookDtos - ); - } + return new PagedResultDto( + totalCount, + bookDtos + ); + } - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } - private async Task> - GetAuthorDictionaryAsync(List books) - { - var authorIds = books - .Select(b => b.AuthorId) - .Distinct() - .ToArray(); + private async Task> + GetAuthorDictionaryAsync(List books) + { + var authorIds = books + .Select(b => b.AuthorId) + .Distinct() + .ToArray(); - var queryable = await _authorRepository.GetQueryableAsync(); + var queryable = await _authorRepository.GetQueryableAsync(); - var authors = await AsyncExecuter.ToListAsync( - queryable.Where(a => authorIds.Contains(a.Id)) - ); + var authors = await AsyncExecuter.ToListAsync( + queryable.Where(a => authorIds.Contains(a.Id)) + ); - return authors.ToDictionary(x => x.Id, x => x); - } + return authors.ToDictionary(x => x.Id, x => x); } } ``` @@ -625,76 +617,76 @@ 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; + private readonly IAuthorAppService _authorAppService; + + public BookAppService_Tests() { - private readonly IBookAppService _bookAppService; - private readonly IAuthorAppService _authorAppService; + _bookAppService = GetRequiredService(); + _authorAppService = GetRequiredService(); + } - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - _authorAppService = GetRequiredService(); - } + [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" && + b.AuthorName == "George Orwell"); + } - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); + [Fact] + public async Task Should_Create_A_Valid_Book() + { + var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); + var firstAuthor = authors.Items.First(); - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984" && - b.AuthorName == "George Orwell"); - } + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + AuthorId = firstAuthor.Id, + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); - [Fact] - public async Task Should_Create_A_Valid_Book() - { - var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); - var firstAuthor = authors.Items.First(); + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } - //Act - var result = await _bookAppService.CreateAsync( + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( new CreateUpdateBookDto { - AuthorId = firstAuthor.Id, - Name = "New test book 42", + Name = "", Price = 10, - PublishDate = System.DateTime.Now, + 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(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); - } + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); } } ``` @@ -708,7 +700,7 @@ namespace Acme.BookStore.Books ### The Book List -Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add the following column definition between the `name` and `type` columns: +Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add an `authorName` column between the `name` and `type` columns: ````js ... @@ -727,7 +719,7 @@ Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.B title: l('Type'), data: "type", render: function (data) { - return l('Enum:BookType:' + data); + return l('Enum:BookType.' + data); } }, ... @@ -735,7 +727,7 @@ Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.B When you run the application, you can see the *Author* column on the table: -![bookstore-added-author-to-book-list](images/bookstore-added-author-to-book-list.png) +![bookstore-added-author-to-book-list](images/bookstore-added-author-to-book-list-2.png) ### Create Modal @@ -753,61 +745,60 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Books; + +public class CreateModalModel : BookStorePageModel { - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateBookViewModel Book { get; set; } + [BindProperty] + public CreateBookViewModel Book { get; set; } - public List Authors { get; set; } + public List Authors { get; set; } - private readonly IBookAppService _bookAppService; + private readonly IBookAppService _bookAppService; - public CreateModalModel( - IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } + public CreateModalModel( + IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } - public async Task OnGetAsync() - { - Book = new CreateBookViewModel(); + public async Task OnGetAsync() + { + Book = new CreateBookViewModel(); - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync( - ObjectMapper.Map(Book) - ); - return NoContent(); - } + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync( + ObjectMapper.Map(Book) + ); + return NoContent(); + } - public class CreateBookViewModel - { - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } + public class CreateBookViewModel + { + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } - [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; } } } ``` @@ -832,66 +823,65 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Books; + +public class EditModalModel : BookStorePageModel { - public class EditModalModel : BookStorePageModel - { - [BindProperty] - public EditBookViewModel Book { get; set; } + [BindProperty] + public EditBookViewModel Book { get; set; } - public List Authors { get; set; } + public List Authors { 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(Guid id) - { - var bookDto = await _bookAppService.GetAsync(id); - Book = ObjectMapper.Map(bookDto); + public async Task OnGetAsync(Guid id) + { + var bookDto = await _bookAppService.GetAsync(id); + Book = ObjectMapper.Map(bookDto); - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync( - Book.Id, - ObjectMapper.Map(Book) - ); + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync( + Book.Id, + ObjectMapper.Map(Book) + ); - return NoContent(); - } + return NoContent(); + } - public class EditBookViewModel - { - [HiddenInput] - public Guid Id { get; set; } + public class EditBookViewModel + { + [HiddenInput] + public Guid Id { get; set; } - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } - [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; } } } ``` @@ -937,7 +927,7 @@ CreateMap(); You can run the application and try to create a new book or update an existing book. You will see a drop down list on the create/update form to select the author of the book: -![bookstore-added-authors-to-modals](images/bookstore-added-authors-to-modals.png) +![bookstore-added-authors-to-modals](images/bookstore-added-authors-to-modals-2.png) {{else if UI=="NG"}} @@ -966,13 +956,13 @@ Book list page change is trivial. Open the `/src/app/book/book.component.html` a When you run the application, you can see the *Author* column on the table: -![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular.png) +![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular-2.png) ### Create/Edit Forms The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below: -![bookstore-angular-author-selection](images/bookstore-angular-author-selection.png) +![bookstore-angular-author-selection](images/bookstore-angular-author-selection-2.png) Added the Author dropdown as the first element in the form. @@ -1114,7 +1104,7 @@ It is very easy to show the *Author Name* in the book list. Open the `/Pages/Boo When you run the application, you can see the *Author* column on the table: -![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors.png) +![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors-2.png) ### Create Book Modal @@ -1136,6 +1126,21 @@ protected override async Task OnInitializedAsync() * It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. +Override the `OpenCreateModalAsync` method and adding the following code: + +````csharp +protected override async Task OpenCreateModalAsync() +{ + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateModalAsync(); + NewEntity.AuthorId = authorList.First().Id; +} +```` + The final `@code` block should be the following: ````csharp @@ -1157,6 +1162,17 @@ The final `@code` block should be the following: await base.OnInitializedAsync(); authorList = (await AppService.GetAuthorLookupAsync()).Items; } + + protected override async Task OpenCreateModalAsync() + { + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateModalAsync(); + NewEntity.AuthorId = authorList.First().Id; + } } ```` @@ -1166,7 +1182,6 @@ Finally, add the following `Field` definition into the `ModalBody` of the *Creat @L["Author"] -
+
*
-
+
*
-
+
* - {%{{{ '::Actions' | abpLocalization }}}%} + {%{{{ '::Actions' | abpLocalization }}}%}
```` @@ -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. diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md index 7d7c4e2d94..40b334808c 100644 --- a/docs/en/Tutorials/Part-6.md +++ b/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 { - public class Author : FullAuditedAggregateRoot - { - 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 CreateAsync( + [NotNull] string name, + DateTime birthDate, + [CanBeNull] string shortBio = null) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); - public async Task 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 - { - Task FindByNameAsync(string name); +namespace Acme.BookStore.Authors; - Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null - ); - } +public interface IAuthorRepository : IRepository +{ + Task FindByNameAsync(string name); + + Task> 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 diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md index 27d46bd4ed..82f01bf963 100644 --- a/docs/en/Tutorials/Part-7.md +++ b/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, + IAuthorRepository { - public class EfCoreAuthorRepository - : EfCoreRepository, - IAuthorRepository + public EfCoreAuthorRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) { - public EfCoreAuthorRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> 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> 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, + IAuthorRepository { - public class MongoDbAuthorRepository - : MongoDbRepository, - IAuthorRepository + public MongoDbAuthorRepository( + IMongoDbContextProvider dbContextProvider + ) : base(dbContextProvider) { - public MongoDbAuthorRepository( - IMongoDbContextProvider dbContextProvider - ) : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable - .WhereIf>( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .As>() - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } + } + + public async Task FindByNameAsync(string name) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .As>() + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); } } ``` diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md index d8dd4bc5b1..24dafffdb9 100644 --- a/docs/en/Tutorials/Part-8.md +++ b/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 GetAsync(Guid id); + Task GetAsync(Guid id); - Task> GetListAsync(GetAuthorListDto input); + Task> GetListAsync(GetAuthorListDto input); - Task CreateAsync(CreateAuthorDto input); + Task 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 { - public class AuthorDto : EntityDto - { - 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 _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository 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(); + } - public AuthorAppService_Tests() - { - _authorAppService = GetRequiredService(); - } + [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(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(async () => - { - await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Douglas Adams", - BirthDate = DateTime.Now, - ShortBio = "..." - } - ); - }); - } - - //TODO: Test other methods... + }); } + + //TODO: Test other methods... } ```` diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md index 4e7914eba1..ba67e2d258 100644 --- a/docs/en/Tutorials/Part-9.md +++ b/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 OnPostAsync() - { - var dto = ObjectMapper.Map(Author); - await _authorAppService.CreateAsync(dto); - return NoContent(); - } + public async Task OnPostAsync() + { + var dto = ObjectMapper.Map(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(); + CreateMap(); - // ADD a NEW MAPPING - CreateMap(); - } + // ADD a NEW MAPPING + CreateMap(); } } ```` "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); - } + public async Task OnGetAsync(Guid id) + { + var authorDto = await _authorAppService.GetAsync(id); + Author = ObjectMapper.Map(authorDto); + } - public async Task OnPostAsync() - { - await _authorAppService.UpdateAsync( - Author.Id, - ObjectMapper.Map(Author) - ); + public async Task OnPostAsync() + { + await _authorAppService.UpdateAsync( + Author.Id, + ObjectMapper.Map(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(); + CreateMap(); - CreateMap(); + CreateMap(); - // ADD THESE NEW MAPPINGS - CreateMap(); - CreateMap(); - } + // ADD THESE NEW MAPPINGS + CreateMap(); + CreateMap(); } } ``` @@ -725,7 +728,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo
@@ -747,7 +750,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo aria-haspopup="true" ngbDropdownToggle > - {%{{{ '::Actions' | abpLocalization }}}%} + {%{{{ '::Actions' | abpLocalization }}}%}
-
+
* LH - - + +

@L["Authors"]

- - + @if (CanCreateAuthor) {