diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index 5b6c46073e..fa15580b78 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch prerel-9.1 with rel-9.0 +name: Merge branch dev with prerel-9.1 on: push: branches: - - rel-9.0 + - prerel-9.1 permissions: contents: read jobs: - merge-prerel-9-1-with-rel-9-0: + merge-dev-with-prerel-9-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 @@ -15,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: prerel-9.1 + ref: dev - name: Reset promotion branch run: | - git fetch origin rel-9.0:rel-9.0 - git reset --hard rel-9.0 + git fetch origin prerel-9.1:prerel-9.1 + git reset --hard prerel-9.1 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-9-0/${{github.run_number}} - title: Merge branch prerel-9.1 with rel-9.0 - body: This PR generated automatically to merge prerel-9.1 with rel-9.0. Please review the changed files before merging to prevent any errors that may occur. + branch: auto-merge/prerel-9-0/${{github.run_number}} + title: Merge branch dev with prerel-9.1 + body: This PR generated automatically to merge dev with prerel-9.1. Please review the changed files before merging to prevent any errors that may occur. reviewers: maliming draft: true token: ${{ github.token }} @@ -34,5 +34,5 @@ jobs: GH_TOKEN: ${{ secrets.BOT_SECRET }} run: | gh pr ready - gh pr review auto-merge/rel-9-0/${{github.run_number}} --approve - gh pr merge auto-merge/rel-9-0/${{github.run_number}} --merge --auto --delete-branch + gh pr review auto-merge/prerel-9-0/${{github.run_number}} --approve + gh pr merge auto-merge/prerel-9-0/${{github.run_number}} --merge --auto --delete-branch diff --git a/Directory.Packages.props b/Directory.Packages.props index f7d4cf2397..0b60bf9e60 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + @@ -39,8 +39,8 @@ - - + + @@ -106,9 +106,10 @@ - - - + + + + @@ -120,11 +121,11 @@ - - - - - + + + + + @@ -166,7 +167,8 @@ - + + @@ -176,4 +178,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 31db04cba2..7cf41d47eb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ABP Framework ![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) 🔹 [![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://abp.io/docs/latest/release-info/nightly-builds) 🔹 -[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) 🔹 [![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/abpframework/abp/blob/dev/CODE_OF_CONDUCT.md) 🔹 [![CLA Signed](https://cla-assistant.io/readme/badge/abpframework/abp)](https://cla-assistant.io/abpframework/abp) 🔹 [![Discord Shield](https://discord.com/api/guilds/951497912645476422/widget.png?style=shield)](https://discord.gg/abp) +[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) 🔹 [![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/abpframework/abp/blob/dev/CODE_OF_CONDUCT.md) 🔹 [![CLA Signed](https://cla-assistant.io/readme/badge/abpframework/abp)](https://cla-assistant.io/abpframework/abp) 🔹 [![Discord Shield](https://discord.com/api/guilds/951497912645476422/widget.png?style=shield)](https://abp.io/join-discord) [ABP](https://abp.io/) offers an **opinionated architecture** to build enterprise software solutions with **best practices** on top of the **.NET** and the **ASP.NET Core** platforms. It provides the fundamental infrastructure, production-ready startup templates, pre-built application modules, UI themes, tooling, guides and documentation to implement that architecture properly and **automate the details** and repetitive works as much as possible. @@ -121,4 +121,4 @@ GitHub repository stars are an important indicator of popularity and the size of ## Discord Server -We have a Discord server where you can chat with other ABP users. Share your ideas, report technical issues, showcase your creations, share the tips that worked for you and catch up with the latest news and announcements about ABP Framework. Join 👉 https://discord.gg/abp. +We have a Discord server where you can chat with other ABP users. Share your ideas, report technical issues, showcase your creations, share the tips that worked for you and catch up with the latest news and announcements about ABP Framework. Join 👉 https://abp.io/join-discord. diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json index 44ab5b3e31..db3249e881 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json @@ -13,6 +13,9 @@ "ManageAccount": "My Account | ABP.IO", "ManageYourProfile": "Manage your profile", "ReturnToApplication": "Return to application", - "IdentityUserNotAvailable:Deleted": "This email address is not available. Reason: Already deleted." + "IdentityUserNotAvailable:Deleted": "This email address is not available. Reason: Already deleted.", + "SelectYourOrganization": "Select your organization", + "PleaseSelectOrganization": "Please select an organization to continue", + "Continue": "Continue" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 9fb30d3fda..ea150de47e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -22,6 +22,7 @@ "Permission:Accounting": "Accounting", "Permission:Accounting:Quotation": "Quotation", "Permission:Accounting:Invoice": "Invoice", + "Permission:Export" : "Export", "Menu:Organizations": "Organizations", "Menu:Accounting": "Accounting", "Menu:Packages": "Packages", @@ -511,6 +512,7 @@ "QuotationTemplate.BankAccount": "Our bank account information can be found at {0}", "Permission:Raffles": "Raffle", "Permission:Draw": "Draw", + "Permission:ExportAttendeesAsExcel": "Export at attendees as Excel", "Menu:Raffles": "Raffles", "RaffleIsNotDrawable": "Raffle is not drawable", "WinnerCountMustBeGreaterThanZero": "Winner count must be greater than zero", @@ -649,6 +651,22 @@ "Permission:HeroSections": "Hero Sections", "RedirectLink": "Redirect link", "HeroSectionsDeletionConfirmationMessage": "Are you sure you want to delete the hero section?", - "AbpStudioName": "Abp Studio name" + "AbpStudioName": "ABP Studio name", + "Permission:EditAttendees": "Edit Attendees", + "AttendeesCount": "Attendees Count", + "CreateQRCode": "Create QR Code", + "DrawTV": "Public draw on the TV", + "DrawModal": "Private draw on the modal", + "SetAsDrawable": "Set as drawable", + "SetAsNoDrawable": "Set as non-drawable", + "SetAsCompleted": "Set as completed", + "RemoveAllWinners": "Remove all winners", + "EditWinners": "Edit winners", + "EditAttendees": "Edit attendees", + "ExportAttendeesAsExcel": "Export attendees as Excel", + "DuplicateRaffle": "Duplicate raffle", + "Menu:RedisManagement": "Redis Management", + "RedisManagement": "Redis Management", + "Permission:RedisManagement": "Redis Management" } -} \ No newline at end of file +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index ed7781b476..4890226d9a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -242,14 +242,14 @@ "ReturnOnInvestment": "Return on Investment", "PromotionalOffers": "Promotional Offers", "PromotionalOffersDefinition": "Discounts, seasonal campaigns, etc.", - "EventsDefinition": "Community Talks, Webinars, ABP .NET Conference, etc.", + "EventsDefinition": "Community Talks, Webinars, ABP DOTNET Conference, etc.", "ReleaseNotesDefinition": "ABP.IO Platform releases, new products, etc.", "Newsletter": "Newsletter", "NewsletterDefinition": "Blog posts, community news, etc.", "OrganizationOverview": "Organization Overview", "EmailPreferences": "Email Preferences", "VideoCourses": "Essential Videos", - "DoYouAgreePrivacyPolicy": "By clicking Subscribe button you agree to the Terms & Conditions and Privacy Policy.", + "DoYouAgreePrivacyPolicy": "By clicking Subscribe button you agree to the Terms & Conditions and Privacy Policy.", "AbpConferenceDescription": "ABP Conference is a virtual event for .NET developers to learn and connect with the community.", "Mobile": "Mobile", "MetaTwitterCard": "summary_large_image" diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index ae040e7fd2..4856813fe1 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -1211,6 +1211,5 @@ "TrainingDescription": "We are offering the following training packages for who want to get expertise on the ABP Framework and the ABP.", "PurchaseDevelopers": "developers", "LinkExpiredMessage": "The payment link has expired! Contact us at sales@volosoft.com to update the link or click here to navigate to the contact page." - } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 987757cd89..08e1687357 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -446,6 +446,7 @@ "PackageDetailPage_InstallingUsingPMCDescription1": "Open the Package Manager Console in Visual Studio (Tools -> Nuget Package Manager -> Package Manager Console) and execute the following command", "UIOptions": "UI Options", "Testimonials": "Testimonials", + "TestimonialsDescription": "Our clients' feedback is invaluable to us. Discover what they have to say about their experience working with us.", "CoolestCompaniesUseABPFramework": "Coolest Companies Use ABP Framework", "Index_Page_Testimonial_1": "ABP Framework is not just a tool but a catalyst that has accelerated my growth as a developer. It has made it possible for me to build new features faster than ever before, reminiscent of the experiences of other users. The unified coding pattern has streamlined my projects, giving me more time to focus on creating rather than troubleshooting.\nI would say the ABP Framework has been the cornerstone of my early professional journey. It has facilitated my transition from an aspiring developer to a confident professional ready to make a mark in the software world. I am looking forward to the exciting projects that await me, knowing that ABP will be there to guide me. It is more than just a product; it's a partner in success.", "Index_Page_Testimonial_2": "ABP Framework is not only a framework, it is also a guidance for project development/management, because it provides DDD, GenericRepository, DI, Microservice, Modularity trainings. Even if you are not going to use framework itself, you can develop yourself with abp.io/docs which is well and professionally prepared. (OpenIddict, Redis, Quartz etc.)\nBecause many thing pre-built, it shortens project development time significantly. (Such as login page, exception handling, data filtering-seeding, audit logging, localization, auto api controller etc.)\nAs an example from our app, i have used Local Event Bus for stock control. So, I am able to manage order movements by writing stock handler.\nIt is wonderful not to lose time for CreationTime, CreatorId. They are filled automatically.", @@ -458,7 +459,7 @@ "FullName": "Full name", "CompanySize": "Company size", "TestimonialTitle": "Let's hear your testimonial", - "TestimonialInfo": "What our customers say matters! Tell us about your experience with our product and service. It is recommended to write the testimonial in English to reach a wider audience.", + "TestimonialInfo": "What you say matters! Tell us about your experience with ABP in a few sentences. Please write it in English to reach a wider audience.", "Country": "Country", "TestimonialTextPlaceholder": "Write a brief story about how ABP helped you build and deliver your project.", "PositionPlaceholder": "Your position at your company", @@ -549,7 +550,7 @@ "CommercialLicenses": "Commercial Licenses", "WhatIsDifferencePaidLicenses": "What is the difference between a personal license and other types of paid licenses?", "DifferencePaidLicenseExplanation1": "A non-personal paid license is the standard licensing option for enterprises and commercial entities. Licenses are purchased by the company and can be used by anyone within the organization.", - "DifferencePaidLicenseExplanation2": "Personal License; on the other hand, is a type of license for private individuals/freelancers/independent developers who purchase licenses with their own funds and solely for their own use. The Personal License has some limitations. In this plan, there can only be 1 developer working on the ABP project and no additional developers are allowed to be added later to the project. Downloading the source-code of PRO modules is not allowed in the personal license plan. Also, there is no microservice template and tier (layered) architecture in this plan. Personal License holders can only use the following modules: Account, Audit Logging UI, GDPR, Identity, Language Management, LeptonX PRO, OpenIddict UI and SaaS. Personal License holders cannot use the following modules: Chat, CMS-Kit PRO, File Management, Forms, Payment, Text Template Management, and Twilio SMS. You can access the full module list at abp.io/modules.", + "DifferencePaidLicenseExplanation2": "Personal License; on the other hand, is a type of license for private individuals/freelancers/independent developers who purchase licenses with their own funds and solely for their own use. The Personal License has some limitations. In this plan, there can only be 1 developer working on the ABP project and no additional developers are allowed to be added later to the project. Downloading the source-code of PRO modules is not allowed in the personal license plan. There is no microservice template in this plan. There is no tier architecture (Web and HTTP API layers are physically separated) in this plan. Personal License holders can only use the following modules: Account, Audit Logging UI, GDPR, Identity, Language Management, LeptonX PRO, OpenIddict UI and SaaS. Personal License holders cannot use the following modules: Chat, CMS-Kit PRO, File Management, Forms, Payment, Text Template Management, and Twilio SMS. You can access the full module list at abp.io/modules.", "ReadyToStart": "Ready to start?", "TransformYourIdeasIntoRealityWithOurProfessionalNETDevelopmentServices": "Transform your ideas into reality with our professional .NET development services.", "ReadyToUpgrade": "Ready to upgrade?", @@ -1690,8 +1691,8 @@ "HurryUpLastDay": "Hurry Up! Last Day: {0}", "CreatingCRUDPagesWithABPSuite": "Creating CRUD pages with ABP Suite", "MultipleYearDiscount": "Multiple Year Discount", - "CampaignDiscountText": "New Platform Discount", - "CampaignDiscountName": "New Platform", + "CampaignDiscountText": "Black Friday Discount", + "CampaignDiscountName": "Black Friday", "CampaignName:BlackFriday": "Black Friday", "MultipleOrganizationInfo": "See All Your Organizations", "AbpStudioBetaAccessInfoTitle": "ABP Studio Beta Access", @@ -1866,6 +1867,7 @@ "NewsletterEmailFooterTemplateDeleteSubscription": "If you change your mind, you're always welcome to resubscribe!", "GenerateQuote" : "Generate Quote" , "GeneratePriceQuote": "Generate a Price Quote", - "Qa:QuestionPageTitle": "Support" + "Qa:QuestionPageTitle": "Support", + "SelectedTrainingName" : "Trainings" } -} \ No newline at end of file +} diff --git a/common.props b/common.props index c47c838983..fda9b5e35b 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 9.0.3 - 4.0.4 + 9.1.0-preview + 4.1.0-preview $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md b/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md index b23dab0cd1..dfc5a7d67e 100644 --- a/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md +++ b/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md @@ -255,4 +255,4 @@ We've created an official ABP Discord server so the ABP Community can interact w Thanks to the ABP Community, **700+** people joined our Discord Server so far and it grows every day. -You can join our Discord Server from [here](https://discord.gg/abp), if you haven't yet. \ No newline at end of file +You can join our Discord Server from [here](https://abp.io/join-discord), if you haven't yet. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/POST.md b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/POST.md new file mode 100644 index 0000000000..f06648e586 --- /dev/null +++ b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/POST.md @@ -0,0 +1,223 @@ +# ABP Platform 9.0 Has Been Released Based on .NET 9.0 + +![](cover-image.png) + +Today, we are happy to release the [ABP](https://abp.io/) version **9.0 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 v9.0! Thanks to all of you. + +## Get Started with the 9.0 RC + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). + +By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: + +![](studio-switch-to-preview.png) + +## Migration Guide + +There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v8.x: [ABP Version 9.0 Migration Guide](https://abp.io/docs/9.0/release-info/migration-guides/abp-9-0) + +## What's New with ABP v9.0? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +* Upgraded to .NET 9.0 +* Introducing the **Extension Property Policy** +* Allow wildcards for Redirect Allowed URLs +* Docs Module: Show larger images on the same page +* Google Cloud Storage BLOB Provider +* Removed React Native mobile option from free templates +* Suite: Better naming for multiple navigation properties to the same entity +* CMS Kit Pro: Feedback feature improvements + +### Upgraded to .NET 9.0 + +We've upgraded ABP to .NET 9.0, so you need to move your solutions to .NET 9.0 if you want to use ABP 9.0. You can check [Microsoft’s Migrate from ASP.NET Core 8.0 to 9.0 documentation](https://learn.microsoft.com/en-us/aspnet/core/migration/80-90), to see how to update an existing ASP.NET Core 8.0 project to ASP.NET Core 9.0. + +> **Note:** Since the stable version of .NET 9 hasn't been released yet, we upgraded ABP to .NET v9.0-rc.2. We will update the entire ABP Platform to .NET 9 stable, after Microsoft releases it on November 13-14 with the stable ABP 9.0 release. + +### Introducing the Extension Property Policy + +ABP provides a module entity extension system, which is a high level extension system that allows you to define new properties for existing entities of the depended modules. This is a powerful way to dynamically add additional properties to entities without modifying the core structure. However, managing these properties across different modules and layers can become complex, especially when different policies or validation rules are required. + +**Extension Property Policy** feature allows developers to define custom policies for these properties, such as access control, validation, and data transformation, directly within ABP. + +**Example:** + +```csharp +ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity => +{ + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( //property type: string + "SocialSecurityNumber", //property name + property => + { + //validation rules + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); + + //Global Features + property.Policy.GlobalFeatures = new ExtensionPropertyGlobalFeaturePolicyConfiguration() + { + Features = new[] {"GlobalFeatureName1", "GlobalFeatureName2"}, + RequiresAll = true + }; + + //Features + property.Policy.Features = new ExtensionPropertyFeaturePolicyConfiguration() + { + Features = new[] {"FeatureName1", "FeatureName2"}, + RequiresAll = false + }; + + //Permissions + property.Policy.Permissions = new ExtensionPropertyPermissionPolicyConfiguration() + { + PermissionNames = new[] {"AbpTenantManagement.Tenants.Update", "AbpTenantManagement.Tenants.Delete"}, + RequiresAll = true + }; + } + ); + }); +}); +``` + +### Allow Wildcards for RedirectAllowedURLs + +In this version, we made an improvement to the `RedirectAllowedUrls` configuration, which now allows greater flexibility in defining redirect URLs. Previously, developers faced restrictions when configuring URL redirects. Specifically, the `RedirectAllowedUrls` did not support using **wildcards (*)**, limiting how developers could specify which URLs were permissible for redirects. + +With the new changes in [#20628](https://github.com/abpframework/abp/pull/20628), the restriction has been relaxed, allowing developers to define redirect URLs that include wildcards. This makes it easier to handle scenarios where a broad range of URLs need to be allowed, without explicitly listing each one. + +```json +{ + "App": { + //... + "RedirectAllowedUrls": "http://*.domain,http://*.domain:4567" + } +``` + +### Docs Module: Show Larger Images + +As developers, we rely heavily on clear documentation to understand complex concepts and workflows. Often, an image is worth more than a thousand words, especially when explaining intricate user interfaces, workflows, or code structures. In recognition of this, we recently rolled out an improvement to the Docs Module that enables larger images to be displayed more effectively. + +![](docs-image-larger.png) + +Before this enhancement, images embedded in documentation were often limited in size, which sometimes made it difficult to see the details in the diagrams, screenshots, or other visual contents. Now, images can be displayed at a larger size, offering better clarity and usability. + +> See [https://github.com/abpframework/abp/pull/20557](https://github.com/abpframework/abp/pull/20557) for more information. + +### Google Cloud Storage BLOB Provider + +ABP provides a BLOB Storing System, which allows you to work with BLOBs. This system is typically used to store file contents in a project and read these file contents when they are needed. Since ABP provides an abstraction to work with BLOBs, it also provides some pre-built storage providers such as [Azure](https://abp.io/docs/latest/framework/infrastructure/blob-storing/azure), [Aws](https://abp.io/docs/latest/framework/infrastructure/blob-storing/aws) and [Aliyun](https://abp.io/docs/latest/framework/infrastructure/blob-storing/aliyun). + +In this version, we have introduced a new BLOB Storage Provider for Google Cloud Storage: [`Volo.Abp.BlobStoring.Google`](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Google) + +You can [read the documentation](https://abp.io/docs/9.0/framework/infrastructure/blob-storing/google) for configurations and use Google Cloud Storage as your BLOB Storage Provider easily. + +### Removed React Native Mobile Option From Free Templates + +In this version, we removed the **React Native** mobile option from the open source templates due to maintaining reasons. We updated the related documents and the ABP CLI (both old & new CLI) for this change, and with v9.0, you will not be able to create a free template with react-native as the mobile option. + +> **Note:** Pro templates still provide the **React Native** as the mobile option and we will continue supporting it. + +If you want to access the open-source React-Native template, you can visit the abp-archive repository from [here](https://github.com/abpframework/abp-archive). + +### Suite: Better Naming For Multiple Navigation Properties + +Prior to this version, when you defined multiple (same) navigation properties to same entity, then ABP Suite was renaming them with a duplicate number. + +As an example,let's assume that you have a book with an author and coauthor, prior to this version ABP Suite was creating a DTO class as below: + +```csharp +public class BookWithNavigationPropertiesDto +{ + public BookDto Book { get; set; } + + public AuthorDto Author { get; set; } + + public AuthorDto Author1 { get; set; } +} +``` + +Notice, that since the book entity has two same navigation properties, ABP Suite renamed them with a duplicate number. In this version, ABP Suite will ask you to define a propertyName for the **navigation properties** and you'll be able to specify a meaningful name such as (*CoAuthor*, in this example): + +```csharp +public class BookWithNavigationPropertiesDto +{ + public BookDto Book { get; set; } + + public AuthorDto Author { get; set; } + + //used the specified property name + public AuthorDto CoAuthor { get; set; } +} +``` + +ABP Suite respects the specified property name for the related navigation property and generates codes regarding that (by removing the *Id* postfix for the related places): + +![](suite-navigation-properties.png) + +### CMS Kit Pro: Feedback Feature Improvements + +In this version, we revised the [CMS Kit's Feedback Feature](https://abp.io/docs/9.0/modules/cms-kit-pro/page-feedback) and as a result, we made the following improvements: + +* A new **auto-handle** setting has been added to the settings page. When this feature is enabled, if feedback is submitted without a user note, the feedback is automatically marked as handled. +* You can now require users to enter a note when submitting negative feedback. This can be configured in the settings page, ensuring that users provide context when they submit critical feedback. +* We've added a feedback user ID that is saved in local storage. This allows you to track the number of unique users submitting feedback or determine if the same user is sending new feedback on updated documents. + +> For further information about the Page Feedback System, please refer to the [documentation](https://abp.io/docs/9.0/modules/cms-kit-pro/page-feedback). + +## Community News + +### Join ABP at the .NET Conf 2024! + +ABP is excited to sponsor the [14th annual .NET Conf](https://www.dotnetconf.net/)! We've proudly supported the .NET community for years and recognize the importance of this premier virtual event. Mark your calendars for November 12-14, 2024, and join us for 3 incredible days of learning, networking, and fun. + +![](dotnet-conf-2024.png) + +Also, don't miss out on the co-founder of [Volosoft](https://volosoft.com/) and Lead Developer of [ABP](https://abp.io/), [Halil Ibrahim Kalkan](https://x.com/hibrahimkalkan)'s talk about "Building Modular Monolith Applications with ASP.NET Core and ABP Studio" at 10:00 - 10:30 AM GMT+3 on Thursday, November 14. + +### ABP Team Attended the .NETDeveloperDays 2024 + +We are thrilled to announce that we sponsored the [.NETDevelopersDays 2024](https://developerdays.eu/warsaw/) event. It's one of the premier conferences for .NET developers with **over 1.000 attendees**, **50+ expert speakers**, and **40+ sessions and workshops**. + +![](dotnet-developer-days-2024.jpg) + +Core team members of the ABP Framework, [Halil Ibrahim Kalkan](https://twitter.com/hibrahimkalkan), [İsmail Çağdaş](https://x.com/ismcagdas), [Enis Necipoğlu](https://x.com/EnisNecipoglu), and [Tarık Özdemir](https://x.com/mtozdemir) attended [.NETDevelopersDays 2024](https://developerdays.eu/warsaw/) on October 22-23, 2024 at Warsaw, Poland. + +These 2 days with the team were all about chatting and having fun with amazing attendees and speakers. We met with talented and passionate software developers and introduced the [ABP](https://github.com/abpframework/abp) - web application framework built on ASP.NET Core - to them. + +Also, we made a raffle and gifted an Xbox Series S to the lucky winner at the event: + +![](abp-team-raffle.jpg) + +Thanks to everyone who joined the fun and visited at our booth :) + +### New ABP Community Articles + +There are exciting articles contributed by the ABP community as always. I will highlight some of them here: + +* [Alper Ebiçoğlu](https://twitter.com/alperebicoglu) has created **five** new community articles: + * [When to Use Cookies, When to Use Local Storage?](https://abp.io/community/articles/when-to-use-cookies-when-to-use-local-storage-uexsjunf) + * [.NET 9 Performance Improvements Summary](https://abp.io/community/articles/.net-9-performance-improvements-summary-gmww3gl8) + * [ASP.NET Core SignalR New Features — Summary](https://abp.io/community/articles/asp.net-core-signalr-new-features-summary-kcydtdgq) + * [Difference Between "Promise" and "Observable" in Angular](https://abp.io/community/articles/difference-between-promise-and-observable-in-angular-bxv97pkc) + * [ASP.NET Core Blazor 9.0 New Features Summary 🆕](https://abp.io/community/articles/asp.net-core-blazor-9.0-new-features-summary--x0fovych) +* [Mohammad AlMohammad AlMahmoud](https://abp.io/community/members/Mohammad97Dev) has created **two** new community articles: + * [Implementing Multi-Language Functionality With ABP Framework](https://abp.io/community/articles/implementing-multilanguage-functionality-with-abp-framework-loq7kfx4) + * [Configure Quartz.Net in Abp FrameWork](https://abp.io/community/articles/configure-quartz.net-in-abp-framework-3bveq4y1) +* [.NET Aspire vs ABP Studio: Side by Side](https://abp.io/community/articles/.net-aspire-vs-abp-studio-side-by-side-t1c73d1l) by [Halil İbrahim Kalkan](https://twitter.com/hibrahimkalkan) +* [PoC of using GrapesJS for ABPs CMS Kit](https://abp.io/community/articles/poc-of-using-grapesjs-for-abps-cms-kit-1rmv4q41) by [Jack Fistelmann](https://abp.io/community/members/jfistelmann) +* [ABP-Powered Web App with Inertia.js, React, and Vite](https://abp.io/community/articles/abppowered-web-app-with-inertia.js-react-and-vite-j7cccvad) by [Anto Subash](https://antosubash.com/) +* [Multi-Tenancy Support in Angular Apps with ABP.IO](https://abp.io/community/articles/multitenancy-support-in-angular-apps-with-abp.io-lw9l36c5) by [HeadChannel Team](https://headchannel.co.uk/) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://abp.io/community/posts/submit) to the ABP Community. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/9.0/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.0 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/abp-team-raffle.jpg b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/abp-team-raffle.jpg new file mode 100644 index 0000000000..1210f13283 Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/abp-team-raffle.jpg differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/cover-image.png b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/cover-image.png new file mode 100644 index 0000000000..f272a8d463 Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/docs-image-larger.png b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/docs-image-larger.png new file mode 100644 index 0000000000..95b999560e Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/docs-image-larger.png differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-conf-2024.png b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-conf-2024.png new file mode 100644 index 0000000000..37ddf06eb4 Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-conf-2024.png differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-developer-days-2024.jpg b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-developer-days-2024.jpg new file mode 100644 index 0000000000..eb9fe26dfd Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/dotnet-developer-days-2024.jpg differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..32f6d01edb Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/studio-switch-to-preview.png differ diff --git a/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/suite-navigation-properties.png b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/suite-navigation-properties.png new file mode 100644 index 0000000000..4d329ccc7b Binary files /dev/null and b/docs/en/Blog-Posts/2024-10-23 v9_0_Preview/suite-navigation-properties.png differ diff --git a/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/community-talks.png b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/community-talks.png new file mode 100644 index 0000000000..7263b68e10 Binary files /dev/null and b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/community-talks.png differ diff --git a/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/cover-image.png b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/cover-image.png new file mode 100644 index 0000000000..f272a8d463 Binary files /dev/null and b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/cover-image.png differ diff --git a/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/post.md b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/post.md new file mode 100644 index 0000000000..26850bc57f --- /dev/null +++ b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/post.md @@ -0,0 +1,93 @@ +# ABP.IO Platform 9.0 Has Been Released Based on .NET 9.0 + +![](cover-image.png) + +Today, [ABP](https://abp.io/) 9.0 stable version has been released based on [.NET 9.0](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). You can create solutions with ABP 9.0 starting from ABP Studio v0.9.11 or by using the ABP CLI as explained in the following sections. + +## What's New With Version 9.0? + +All the new features were explained in detail in the [9.0 RC Announcement Post](https://abp.io/blog/announcing-abp-9-0-release-candidate), so there is no need to review them again. You can check it out for more details. + +## Getting Started with 9.0 + +### Creating New Solutions + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli) to create new solutions. + +By default, ABP Studio uses stable versions to create solutions. Therefore, it will be creating the solution with the latest stable version, which is v9.0 for now, so you don't need to specify the version. **You can create solutions with ABP 9.0 starting from v0.9.11.** + +### How to Upgrade an Existing Solution + +You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained: + +### Upgrading via ABP Studio + +If you are already using the ABP Studio, you can upgrade it to the latest version to align it with ABP v9.0. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info. + +After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Switch to stable** action button to instantly upgrade your solution: + +![](switch-to-stable.png) + +> Please note that ABP CLI & ABP Studio only upgrade the related ABP packages, so you need to upgrade the other packages for .NET 9.0 manually. + +### Upgrading via ABP CLI + +Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version. + +If you haven't installed it yet, you can run the following command: + +```bash +dotnet tool install -g Volo.Abp.Studio.Cli +``` + +Or to update the existing CLI, you can run the following command: + +```bash +dotnet tool update -g Volo.Abp.Studio.Cli +``` + +After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows: + +```bash +abp update +``` + +You can run this command in the root folder of your solution to update all ABP related packages. + +> Please note that ABP CLI & ABP Studio only upgrade the related ABP packages, so you need to upgrade the other packages for .NET 9.0 manually. + +## Migration Guides + +There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v8.x: [ABP Version 9.0 Migration Guide](https://abp.io/docs/9.0/release-info/migration-guides/abp-9-0) + +## Community News + +### Highlights from .NET 9.0 + +Our team has closely followed the ASP.NET Core and Entity Framework Core 9.0 releases, read Microsoft's guides and documentation, and adapted the changes to our ABP.IO Platform. We are proud to say that we've shipped the ABP 9.0 based on .NET 9.0 just after Microsoft's .NET 9.0 release. + +In addition to the ABP's .NET 9.0 upgrade, our team has created many great articles to highlight the important features coming with ASP.NET Core 9.0 and Entity Framework Core 9.0. + +> You can read [this post](https://volosoft.com/blog/Highlights-for-ASP-NET-Entity-Framework-Core-NET-9-0) to see the list of all articles. + +### New ABP Community Articles + +In addition to [the articles to highlight .NET 9.0 features written by our team](https://volosoft.com/blog/Highlights-for-ASP-NET-Entity-Framework-Core-NET-9-0), here are some of the recent posts added to the [ABP Community](https://abp.io/community): + +* [Video: Building Modular Monolith Applications with ASP.NET Core & ABP Studio](https://abp.io/community/videos/building-modular-monolith-applications-with-asp.net-core-abp-studio-66znukvf) by [Halil İbrahim Kalkan](https://x.com/hibrahimkalkan) +* [How to create your Own AI Bot on WhatsApp Using an ABP.io Template](https://abp.io/community/articles/how-to-create-your-own-ai-bot-on-whatsapp-using-the-abp-framework-c6jgvt9c) by [Michael Kokula](https://abp.io/community/members/Michal_Kokula) +* [ABP Now Supports .NET 9](https://abp.io/community/articles/abp-now-supports-.net-9-zpkznc4f) by [Alper Ebiçoğlu](https://x.com/alperebicoglu) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/submit) to the ABP Community. + +### ABP Community Talks 2024.7: What’s New with .NET 9 & ABP 9? + +![](community-talks.png) + +In this episode of ABP Community Talks, 2024.7; we will dive into the features that came with .NET 9.0 with [Alper Ebicoglu](https://github.com/ebicoglu), [Engincan Veske](https://github.com/EngincanV), [Berkan Sasmaz](https://github.com/berkansasmaz) and [Ahmet Faruk Ulu](https://github.com/ahmetfarukulu). + +## 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/9.0/Road-Map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.0 and provide feedback to help us release more stable versions. + +Thanks for being a part of this community! diff --git a/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/switch-to-stable.png b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/switch-to-stable.png new file mode 100644 index 0000000000..30883ebf92 Binary files /dev/null and b/docs/en/Blog-Posts/2024-11-19 v9_0_Release_Stable/switch-to-stable.png differ diff --git a/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md b/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md new file mode 100644 index 0000000000..e4c37eec07 --- /dev/null +++ b/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md @@ -0,0 +1,67 @@ +# ABP Studio Goes AOT: Faster Startups with Ready-to-Run (R2R) Publishing + +We're excited that [ABP Studio](https://abp.io/studio) now supports [Ready-to-Run (R2R) publishing](https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run) (starting from v0.9.16+), a hybrid form of ahead-of-time (AOT) compilation. This enhancement significantly improves the startup time and overall performance of ABP Studio, making it faster and more performant than ever before. + +Let's dive into what R2R publishing is, how it works, and the benefits it brings to ABP Studio. + +## What is Ready-to-Run (R2R) Publishing? + +Ready-to-Run (R2R) is a form of AOT compilation available in the .NET ecosystem. Unlike traditional just-in-time (JIT) compilation, R2R precompiles parts of your application to native code before deployment. This precompiled code helps reduce the startup time by minimizing the work needed during runtime. + +However, R2R isn't a complete AOT compilation. Instead, it's a hybrid approach because it stores both: + +* **Native code for precompiled methods** (to improve startup time and performance) + +* **Intermediate Language (IL) code** for methods that may need further JIT compilation + +This hybrid nature is why R2R binaries are typically larger. For ABP Studio, the storage size increased by ~150 MB with R2R enabled, but the trade-off is well worth it for the performance and startup-time gains. + +## How R2R (Ready-to-Run) Improves ABP Studio + +### Faster Startup Time 🚀 + +One of the biggest advantages of R2R publishing is its impact on startup times. In our local tests, enabling R2R resulted in startup times being **reduced by 2.5x** ⬇️. + +This means you can get to work faster, without waiting for the application to being startup from the beginning. Whether you're launching ABP Studio to manage projects, generate code, or deploy applications, the improved responsiveness is noticeable. + +### Performance Enhancements 📈 + +In addition to faster startups, R2R publishing contributes to overall performance improvements. By precompiling frequently used methods, R2R reduces the workload on the JIT compiler during execution, leading to smoother and more efficient operations. + +### Trade-offs: Increased Storage Size 🆙 + +With great performance comes a slight trade-off: storage size. R2R binaries include both **native** and **IL code**, which increases the file size. In the case of ABP Studio, the storage footprint increased by ~150 MB. However, the substantial improvements in speed and responsiveness make this a worthwhile investment. + +## How to Enable R2R Publishing in Your Applications? + +If you're developing applications and want to benefit from R2R, here's a quick guide on how to enable it in your .NET projects: + +1. You can add the following configuration to your final project's `.csproj` file: + +```xml + + true + +``` + +2. Then, publish your application with the `dotnet publish` command: + +```bash +dotnet publish -c Release +``` + +Alternatively, you can specify the _PublishReadyToRun_ flag directly to the `dotnet publish` command as follows: + +```bash +dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true +``` + +That's it! Your application will now include precompiled native code for faster startup and great performance benefits. + +> Please refer to the [official documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run) before publishing your application with R2R. + +## Conclusion + +As ABP team, we're always looking for ways to improve the developer experience. By adopting **Ready-to-Run (R2R) publishing** for ABP Studio, we're aiming to deliver a faster and more efficient tool for your development needs. + +Stay tuned for more updates and enhancements as we continue to optimize ABP Studio and please provide us with your invaluable feedback. \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md b/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md index b3870ff067..f639d60b0c 100644 --- a/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md +++ b/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md @@ -240,3 +240,7 @@ gRPC on .NET has different approaches, features, configurations and more details * You can find the completed source code here: https://github.com/abpframework/abp-samples/tree/master/GrpcDemo2 * You can also see all the changes I've done in this article here: https://github.com/abpframework/abp-samples/pull/200/files + +## See Also + +* [Consuming gRPC Services from Blazor WebAssembly Application Using gRPC-Web](https://abp.io/community/articles/consuming-grpc-services-from-blazor-webassembly-application-using-grpcweb-dqjry3rv) diff --git a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md index a5f2c694ed..d100838b1a 100644 --- a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md +++ b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md @@ -187,11 +187,21 @@ On the other hand, resolving keyed services from `LazyServiceProvider` is not su ### Automatically Registering Keyed Services -Currently, if you want to register a keyed service, you need to do it manually as we see in the previous sections by using one of the overloads (`.AddKeyedTransient`, `.AddKeyedScoped` and `.AddKeyedSingleton`). +ABP provides the `ExposeKeyedServiceAttribute` to control which keyed services are provided by the related class. -It would be good if we could make this process automatically and not need to manually register services, and for that purpose, I have [created an issue](https://github.com/abpframework/abp/issues/18794) that aims to introduce an attribute, which allows us to automatically register multiple services as keyed services. +For example, if you want to register a keyed service as a transient dependency, you can do it as follows: -You can [follow the issue](https://github.com/abpframework/abp/issues/18794) if you are considering using keyed services in your application and don't want to register them manually. +```csharp +[ExposeKeyedService("taxCalculator")] +[ExposeKeyedService("calculator")] +public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency +{ +} +``` + +> Notice that the ExposeKeyedServiceAttribute only exposes the keyed services. So, you can not inject the ITaxCalculator or ICalculator interfaces in your application without using the FromKeyedServicesAttribute as shown in the example above. If you want to expose both keyed and non-keyed services, you can use the ExposeServicesAttribute and ExposeKeyedServiceAttribute attributes altogether. + +Please refer to the [Dependency Injection document](https://abp.io/docs/latest/framework/fundamentals/dependency-injection#exposekeyedservice-attribute) for further info. ## Summary diff --git a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md index 7d78d476c5..4afe83c7c9 100644 --- a/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md +++ b/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/How to use Aspire with ABP framework.md @@ -300,3 +300,7 @@ After making all our changes, we can run the `AspirationalAbp.AppHost` project. ## Conclusion Combining .NET Aspire with the ABP framework creates a powerful setup for building robust, observable, and feature-rich applications. By integrating Aspire's observability and cloud capabilities with ABP's approach of focusing on your business without repeating yourself, you can develop feature-rich, scalable applications with enhanced monitoring and seamless cloud integration. This guide provides a clear path to set up and configure these technologies, ensuring your applications are well-structured, maintainable, and ready for modern cloud environments. + +## See Also + +* [.NET Aspire vs ABP Studio: Side by Side](https://abp.io/community/articles/.net-aspire-vs-abp-studio-side-by-side-t1c73d1l) diff --git a/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/Post.md b/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/Post.md new file mode 100644 index 0000000000..bac7f69596 --- /dev/null +++ b/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/Post.md @@ -0,0 +1,63 @@ +# When to Use Cookies, When to Use Local Storage? + +![cover](cover.png) + + + +## Cookies vs Local Storage + +When you want to save client-side data on browsers, you can use `Cookies` or `Local Storage` of the browser. While these methods look similar, they have different behaviors. You need to decide based on the specific use-case, security concerns and the data size being stored. I'll clarify the differences between these methods. + + + +## When to use Cookies 🍪? + +1. **Server Communication (e.g: Authentication Tokens):** Cookies are ideal when you need to send data automatically with HTTP requests to the server, such as authentication tokens (JWTs) or session IDs. Cookies can be configured to be sent only to specific domains or paths, making them useful for session management. +2. **Cross-Domain Communication:** Cookies can be shared across subdomains, which is useful when working with multiple subdomains under the same parent domain for microservice architecture. +3. **Expiration Control:** Cookies come with built-in expiration times. You don’t need to manually remove them after a certain period that should expire. +4. **Security:** Cookies can be marked as `HttpOnly` which makes them accessible **only via the server**, not via JavaScript! Also, when you set a cookie attribute, `Secure` it can be sent only over HTTPS, which forces enhanced security for sensitive data. + + +### Considerations for Cookies + +- **Size Limitation:** Cookies are generally limited to around 4KB of data. +- **Security Risks:** Cookies are susceptible to cross-site scripting (XSS) attacks unless marked `HttpOnly`. + + +--- + + +## When to use Local Storage🗄️? + +1. **Client-Side Data Storage:** Local storage is ideal for storing large amounts of data (up to 5–10 MB) that doesn’t need to be sent to the server with every request. For example; *user preferences*, *settings*, or *cached data*. +2. **Persistence:** Data in local storage persists even after the browser is restarted. This behavior makes it useful for long-term storage needs. +3. **No Automatic Server Transmission:** Local storage data is never automatically sent to the server, which can be a security advantage if you don’t want certain data to be exposed to the server or included in the requests. + + +### Considerations for Local Storage + +- **Security Risks:** Local storage is accessible via JavaScript, making it vulnerable to XSS attacks. Sensitive data should not be stored in local storage unless adequately encrypted. + +- **No Expiration Mechanism:** Local storage does not have a built-in expiration mechanism. You must manually remove the data when it’s no longer needed. + + +--- + + + +## Summary + +### Use Cookies + +- For data that needs to be sent to the server with HTTP requests, particularly for session management or authentication purposes. + +### Use Local Storage + +- For storing large amounts of client-side data that doesn’t need to be automatically sent to the server and for data that should persist across browser sessions. + + + +In many cases, you might use both cookies and local storage, depending on the specific requirements of different parts of your application. There are also other places where you can store the client-side data. You can check out [this article](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage) for more information. + + +Happy coding 🧑🏽‍💻 \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/cover.png b/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/cover.png new file mode 100644 index 0000000000..5c7f576575 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-09-Cookies-vs-Local-Storage/cover.png differ diff --git a/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/Post.md b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/Post.md new file mode 100644 index 0000000000..67e451f0df --- /dev/null +++ b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/Post.md @@ -0,0 +1,96 @@ +# .NET 9 Performance Improvements Summary + +With every release, .NET becomes faster & faster! You get these improvements for free by just updating your project to the latest .NET! + +![Cover Image](cover.png) + +It’s very interesting that **20% of these improvements** are implemented by **open-source volunteers** rather than Microsoft employees. These improvements mostly focus on cloud-native and high-throughput applications. I’ll briefly list them below. + +![From Microsoft Blog Post](cited-from-microsoft-blog-post.png) + + + +## 1. Dynamic PGO with JIT Compiler + +* ### What is dynamic PGO? + With “Profile Guided Optimization” the compiler optimizes the code, based on the flow and the way the code executes. It is predicated on the idea that every potential behavior of the code will always transpire. + +* ### What’s Improved? + The tiered compilation, inlining, and dynamic PGO are three ways that .NET 9 optimizes the JIT compiler. This enhances runtime performance and speeds up the time for apps to launch. + +* ### Performance Gains + CPU use is lower during execution; therefore, **startup times are about 15% faster**. + +* ### As a Developer + Faster, smoother deployments with reduced warm-up times... These enhancements reduce latency for applications with complex workflows, particularly in microservices and high-throughput environments. + +* ### How to activate Dynamic PGO? + Add the following to your `csproj` file, or if you have several `csproj` files, you can add it once in `Directory.Build.props` file. Check out [this link](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/compilation#profile-guided-optimization) to understand PGO. + +```xml + + true + +``` + + + +## 2. Library Improvements + +* ### What’s Improved? + + LINQ and JSON serialization, collections and libraries are significantly improved with .NET 9. + +* ### Performance Gains + + **JSON serialization** performance **increases by about 35%**. This helps with heavy data parsing and API requests. Less memory is allocated to `Span` operations as well, and LINQ techniques such as `Where` and `Select` are now faster. + +* ### As a Developer + + This means that apps will be faster, especially those that handle data primarily in JSON or manipulate data with LINQ. + + + +## 3. ASP.NET Core + +* ### What’s Improved? + Kestrel server has undergone significant modifications, mostly in processing the HTTP/2 and HTTP/3 protocols. + +* ### Performance Gains + Now, **Kestrel handles requests up to 20% faster** and **has a 25% reduction in average latency**. Improved connection management and SSL processing also result in overall efficiency gains. + +* ### As a Developer + These modifications result in less resource use, quicker response times for web applications, and more seamless scaling in high-traffic situations. + + + +## 4. Garbage Collection & Memory Management + +* ### What’s Improved? + NET 9’s garbage collection (GC) is more effective, especially for apps with high allocation rates. + +* ### Performance Gains + Applications experience smoother **garbage collection cycles with 8–12% less memory overhead**, which lowers latency and delays. + +* ### As a Developer + The performance will be more reliable and predictable for developers as there will be fewer memory-related bottlenecks, particularly in applications that involve frequent object allocations. + + + +## 5. Native AOT Compilation + +* ### What’s Improved? + Native AOT (Ahead-of-Time) compilation is now more efficient by lowering memory footprint and cold-start times. This leads to better support for cloud-native applications. + +* ### Performance Gains + Native AOT apps now have faster cold launches and use **30–40% less memory**. This improvement focuses on containerized applications. + +--- + + + +**References:** + +* [Microsoft .NET blog post](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/). +* [What’s new in the .NET 9 runtime?](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#performance-improvements) + diff --git a/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cited-from-microsoft-blog-post.png b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cited-from-microsoft-blog-post.png new file mode 100644 index 0000000000..5deb5c12fb Binary files /dev/null and b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cited-from-microsoft-blog-post.png differ diff --git a/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cover.png b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cover.png new file mode 100644 index 0000000000..bec5aa6579 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-09-NET9-Performance-Improvements/cover.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/POST.md b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/POST.md new file mode 100644 index 0000000000..9e996d2099 --- /dev/null +++ b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/POST.md @@ -0,0 +1,138 @@ +# .NET Aspire vs ABP Studio: Side by Side + +In this article, I will compare [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) by [ABP Studio](https://abp.io/docs/latest/studio) by explaining their similarities and differences. + +![cover](cover.png) + +## Introduction + +While .NET Aspire and ABP Studio are tools for different purpose with different scope and they have different approaches to solve the problems, many developers still may confuse since they also have some similar functionalities and solves some common problems. + +In this article, I will clarify all, and you will have a clear understanding of what are the similarities and differences of them. Let's start by briefly define what are .NET Aspire and ABP Studio. + +### What is .NET Aspire? + +**[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/)** is a **cloud-ready framework** designed to simplify building distributed, observable, and production-ready applications. It provides a set of opinionated tools and NuGet packages tailored for cloud-native concerns like **orchestration**, **service integration** (e.g., Redis, PostgreSQL), and **telemetry**. Aspire focuses on the **local development experience**, making it easier to manage complex, multi-service apps by **abstracting away configuration details**. + +Here, a screenshot from [.NET Aspire dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview) that is used for application monitoring and inspection: + +![dotnet-aspire-dashboard](dotnet-aspire-dashboard.png) + +### What is ABP Studio? + +**[ABP Studio](https://abp.io/docs/latest/studio)** is a cross-platform **desktop application** designed to **simplify development** on the ABP Framework by **automating various tasks** and offering a streamlined, **integrated development environment**. It allows developers to **build**, **run**, **test**, **monitor**, and **deploy applications** more efficiently. With features like Kubernetes integration and support for complex multi-application systems, ABP Studio **enhances productivity**, especially in **microservice or modular monolith architectures**. + +Here, a screenshot from the ABP Studio [Solution Runner panel](https://abp.io/docs/latest/studio/running-applications) that is used to run, browse, monitor and inspect applications: + +![abp-studio-solution-runner](abp-studio-solution-runner.png) + +## A Brief Comparison + +Before deep diving details, I want to show a **table of features** to compare ABP Studio and .NET Aspire side by side: + +![abp-studio-vs-net-aspire-comparison-table](abp-studio-vs-dotnet-aspire-comparison-table.png) + +## Comparing the Features + +In the next sections, I will go through each feature and explain differences and similarities. + +### Integration Packages + +ABP Framework has tens of integration packages to 3rd-party libraries and services. .NET Aspire also has some library integrations. But these integrations have different purposes: + +* **ABP Framework**'s integrations (like [MongoDB](https://abp.io/docs/latest/framework/data/mongodb), [RabbitMQ](https://abp.io/docs/latest/framework/infrastructure/background-jobs/rabbitmq), [Dapr](https://abp.io/docs/latest/framework/dapr), etc) are integrations for its abstractions and aimed to be **used directly by your application code**. They are complete and sophisticated integrations with the ABP Framework and your codebase. +* **.NET Aspire**'s integrations (like [MongoDB](https://learn.microsoft.com/en-us/dotnet/aspire/database/mongodb-integration), [RabbitMQ](https://learn.microsoft.com/en-us/dotnet/aspire/messaging/rabbitmq-integration), [Dapr](https://learn.microsoft.com/en-us/dotnet/aspire/frameworks/dapr), etc), on the other hand, for simplifying configuration, service discovery, orchestration and monitoring of these tools within .NET Aspire host. Basically, these are mostly for **integrating to .NET Aspire**, not for integrating to your application. + +For example, ABP's [MongoDB](https://abp.io/docs/latest/framework/data/mongodb) integration allows you to use MongoDB over [repository services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories), automatically handles database transactions, [audit logs](https://abp.io/docs/latest/framework/infrastructure/audit-logging), [event publishing](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed) on data saves, dynamic [connection string](https://abp.io/docs/latest/framework/fundamentals/connection-strings) management, [multi-tenancy](https://abp.io/docs/latest/framework/architecture/multi-tenancy) integration and so on. + +On the other hand, .NET Aspire's [MongoDB](https://learn.microsoft.com/en-us/dotnet/aspire/database/mongodb-integration) integration basically adds [MongoDB driver library](https://www.nuget.org/packages/MongoDB.Driver/) to your .NET Aspire host application and configures it so you can discover MongoDB server on runtime, use a MongoDB Docker container and see its health status, logs and traces on .NET Aspire dashboard. + +### Starter Templates + +Both of ABP Studio and .NET Aspire provide **startup solution templates for new applications**. However, there are huge differences between these startup solution templates and their purpose are completely different. + +* ABP Studio provides **production-ready** and [advanced solution templates](https://abp.io/docs/latest/solution-templates) for **layered**, **modular** or **microservice** solution development. They are well configured for **local development** and deploying to **Kubernetes** and other **production environments**. They provide different **UI and database options**, many optional modules and configuration. For example, you can check the [microservice solution template](https://abp.io/docs/latest/solution-templates/microservice/overview) to see how **sophisticated** it is. +* .NET Aspire's [project templates](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/setup-tooling?tabs=windows&pivots=visual-studio#net-aspire-project-templates)' main purpose is to provide a minimal application structure that is **pre-integrated to .NET Aspire** libraries and configured for **local development** environment. + +So, when you start with .NET Aspire project template, you will need to deal with a lot of work to make your solution production and enterprise ready. On the other hand, ABP Studio's solution templates are ready to launch your system from the first day and they provide you a perfect starting point for your new business idea. + +### Monitoring & Application Running + +Monitoring applications and services is an important requirement for building **complex distributed systems**. Both of ABP Studio and .NET Aspire provide **excellent tools** for that purpose. + +* ABP Studio's [Solution Runner panel](https://abp.io/docs/latest/studio/running-applications) provides a powerful UI to run and monitor applications and services. You can see all HTTP requests, distributed events, exceptions and detailed application logs, trace and find problems in your system. You can use its fully functional built-in browser to navigate application UIs easily. You can also create multiple profiles to group and configure the applications for different teams. +* .NET Aspire's [dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview) can be used to see the states of the running applications and containers, explore their console output, logs, traces and metrics to understand what is happing in your distributed system. + +Both tools are pretty useful for monitoring. In addition to monitoring, **ABP Studio offers an advanced UI to control the running applications**, build, start and stop individually or by a group of applications. + +### Architecting / Building Solutions + +One of the unique features of **ABP Studio** is that it **is an architectural tool** that helps you create the structure and architecture of your solution. You can create any kind of application, from **single-layer** simple web applications to **layered multi-application** solutions, from **monolith modular** to **microservice** systems. In the next section, I will briefly explains these architectural features. + +#### Building Modular Monolith Solutions + +With ABP Studio, you can create a new solution, **create modules and establish relations** (dependencies) between modules to architect your overall **modular monolith system** easily. + +Here, a screenshot where we are adding an existing package reference to the Products module of a modular CRM solution: + +![abp-studio-add-existing-package](abp-studio-add-existing-package.png) + +You can see the [Modular Application Development tutorial](https://abp.io/docs/latest/tutorials/modular-crm) to learn how to build such an application step by step. + +#### Building Microservice Solutions + +ABP Studio provides a full featured [microservice startup solution template](https://abp.io/docs/latest/solution-templates/microservice) and the fundamental tooling to build **large-scale microservice systems**. + +Here a screenshot that shows how to add new microservices, API gateways or web applications to a microservice solution: + +![abp-studio-add-new-microservice](abp-studio-add-new-microservice.png) + +.NET Aspire has no such a feature and has no such a plan to provide that kind of architectural solution building experience. + +### Kubernetes Integration + +Another great ABP Studio feature is [Kubernetes Integration](https://abp.io/docs/latest/studio/kubernetes). It allows you to develop your distributed / microservice solutions as integrated to [Kubernetes](https://kubernetes.io/). + +Here, a few tasks you can accomplish using ABP Studio's Kubernetes integration: + +* **Build docker images** of your applications and services +* **Install and uninstall Helm charts** to your Kubernetes cluster +* **Connect to internal services** of your Kubernetes cluster +* **Monitor** services and applications that are running in your Kubernetes cluster +* **Intercept traffic** of a service and redirect requests to your local machine. In that way, you can develop, test and run individual services or applications in your local computer that is **fully integrated** to other services and applications running in Kubernetes. + +ABP Studio's Kubernetes Integration makes microservice development so easy and comfortable. On the other hand, .NET Aspire has no such a Kubernetes integrated development experience. + +## The ABP Platform + +Until now, I directly compared ABP Studio and .NET Aspire features. .NET Aspire is directly built on .NET and ASP.NET Core. However, ABP Studio is not a standalone tool that is built on .NET and ASP.NET Core. It is built on the [ABP Platform](https://abp.io/) (which is built on .NET and ASP.NET Core). + +The following diagram shows ABP Platform components at a glance: + +![abp-overall-diagram](abp-overall-diagram.png) + +So, when you use ABP Studio, you also take full power of the [open source ABP Framework](https://github.com/abpframework/abp) and other ABP Platform features. + +## ABP and .NET Aspire Integration + +I have a good news to you. It is actually possible and pretty easy to make ABP Platform and .NET Aspire working together. + +You can check [@berkansasmaz](https://abp.io/community/members/berkansasmaz)'s great article: **[How to use .NET Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk)**. + +## Licensing + +ABP Studio has a Community Edition which is completely free and available to everyone. It includes many of the features I mentioned here. There is also a commercial edition that is included in [commercial ABP licenses](https://abp.io/pricing). You can [check that blog post](https://abp.io/blog/announcing-abp-studio-general-availability) which clearly explains the license differences and introduces the fundamental ABP Studio features. + +On the other hand, .NET Aspire is a free tool developed and published by Microsoft. It has no commercial version. + +## Conclusion + +Both .NET Aspire and ABP Studio serve distinct purposes, catering to different types of development environments. While .NET Aspire excels in simplifying cloud-native application setups and observability, ABP Studio provides a comprehensive framework for modular monoliths and microservice architectures with full-fledged enterprise level production-ready startup solution templates and integrated tools. + +In the previous section, it was mentioned that it is possible to [use them together](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk). You don't have to select one of them. However, in my opinion, when you use ABP Studio, you won't need .NET Aspire since ABP Studio can do everything and much more. If you have budget, I suggest to purchase a commercial ABP Studio [license](https://abp.io/pricing) so you can fully unlock its power. + +## Resources / Further Reading + +* [ABP Studio documentation](https://abp.io/docs/latest/studio) +* [.NET Aspire documentation](https://learn.microsoft.com/en-us/dotnet/aspire/) +* [How to use .NET Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk) diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-overall-diagram.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-overall-diagram.png new file mode 100644 index 0000000000..16d397466a Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-overall-diagram.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-existing-package.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-existing-package.png new file mode 100644 index 0000000000..a5940a931f Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-existing-package.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-new-microservice.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-new-microservice.png new file mode 100644 index 0000000000..7af8143949 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-add-new-microservice.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-solution-runner.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-solution-runner.png new file mode 100644 index 0000000000..27047da5c6 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-solution-runner.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-vs-dotnet-aspire-comparison-table.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-vs-dotnet-aspire-comparison-table.png new file mode 100644 index 0000000000..8b27070d5c Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/abp-studio-vs-dotnet-aspire-comparison-table.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/cover.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/cover.png new file mode 100644 index 0000000000..cec722e760 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/cover.png differ diff --git a/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/dotnet-aspire-dashboard.png b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/dotnet-aspire-dashboard.png new file mode 100644 index 0000000000..a0401a3fd0 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-11-NET-Aspire-vs-ABP-Studio/dotnet-aspire-dashboard.png differ diff --git a/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/cover.png b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/cover.png new file mode 100644 index 0000000000..68819c12d9 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/cover.png differ diff --git a/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/dog-food.png b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/dog-food.png new file mode 100644 index 0000000000..67294f7596 Binary files /dev/null and b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/dog-food.png differ diff --git a/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/ef-core-upgrade.png b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/ef-core-upgrade.png new file mode 100644 index 0000000000..b91d5edd5f Binary files /dev/null and b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/ef-core-upgrade.png differ diff --git a/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/net-support-policy.png b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/net-support-policy.png new file mode 100644 index 0000000000..ae7afa5d8b Binary files /dev/null and b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/net-support-policy.png differ diff --git a/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/post.md b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/post.md new file mode 100644 index 0000000000..2a75a97df3 --- /dev/null +++ b/docs/en/Community-Articles/2024-10-23-Abp-Net9-Upgrade/post.md @@ -0,0 +1,147 @@ +# ABP Now Supports .NET 9 + +![Cover image](cover.png) + + + +**.NET 9.0.100-rc.2** has been released on **October 8, 2024**. To align with the latest .NET, we also released the ABP Platform [9.0.0-rc.1](https://github.com/abpframework/abp/releases/tag/9.0.0-rc.1) version. +**With this release, ABP now supports .NET 9.** + +The .NET 9 stable version is planned to be released on **November 12, 2024** before the [.NET Conf 2024](https://www.dotnetconf.net/) event. The ABP 9.0 stable version is planned to be released on November 19, 2024. + +--- + +- **Download the .NET 9 runtime** and SDK from the following link: + + [https://dotnet.microsoft.com/en-us/download/dotnet/9.0](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) + +- There are many enhancements and bug fixes with ABP 9.0. Read the ABP 9 announcement: + + https://abp.io/blog/announcing-abp-9-0-release-candidate + +- + Read **our migration ABP 9.0 migration guide** from the following link: + + [abp.io/docs/9.0/release-info/migration-guides/abp-9-0](https://abp.io/docs/9.0/release-info/migration-guides/abp-9-0) + +- The following is the **PR is for the .NET 9 upgrade** in the ABP source code: + + [https://github.com/abpframework/abp/pull/20803](https://github.com/abpframework/abp/pull/20803) + +--- + + + +## .NET 9 Releases + +In the following link, you can find **a list of all .NET 9 releases** with direct links to release notes and announcements/discussions: + +* https://github.com/dotnet/core/discussions/9234 + + + +--- + + + +## ABP Supports Both .NET 8 & .NET 9 + +The ABP 9.0 version fully supports .NET 9 within our new templates and modules. For developers who want to update their ABP packages to the latest but want to keep them in .NET 8, **we support both .NET 8 and .NET 9** in ABP 9. In your host application, you can choose your target framework. + +So you can decide which version you want to use in your startup Host Application’s `` tag. + +In [this link](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp/Volo.Abp.csproj#L7) you can see that netstandard2.0/2.1 and net8/9 are supported. + +```xml + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + + +``` + + + +### New ASP.NET Core Middleware: Static Asset Delivery + +`MapStaticAssets` is a new middleware that helps optimize the delivery of static assets in any ASP.NET Core app, including Blazor apps. With this change, some `JavaScript/CSS/Images` files exist in the [Virtual File System](https://abp.io/docs/latest/framework/infrastructure/virtual-file-system?_redirected=B8ABF606AA1BDF5C629883DF1061649A), but the new ASP.NET Core 9 `MapStaticAssets` can't handle them. You need to add `StaticFileMiddleware` to serve these files. In ABP 9, we added `MapAbpStaticAssetsan `extension method to support the new `MapStaticAssets`. You can read about this new feature at [this link](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#static-asset-delivery-optimization). +ABP’s new extension method is available [here](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs#L129-L198). + +--- + + + +## How to Upgrade from .NET 8 to .NET 9: + +Install the latest .NET 9 SDK from [this link](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). +Upgrade [dotnet-ef](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) tool version with the following command: + +```bash +dotnet tool uninstall --global dotnet-ef && dotnet tool install --global dotnet-ef +``` + +![EF Core Upgrade](ef-core-upgrade.png) + +1. Change all `TargetFramework` tags from `net8.0` to `net9.0`. +2. Upgrade all Microsoft NuGet packages to `9.0.0`. +3. If you have `global.json`, update `dotnet`version to `9.0.0` . +4. Replace`app.UseStaticFiles()` to `app.MapAbpStaticAssets()` in your module classes and startup projects. + [See the related changes in the repository.](https://github.com/abpframework/abp/commit/0f34f6dfcdbeb5d27fd63cf764f1ef13eb9cdfcd) + + + +--- + + + +## What’s new with .NET 9 + +**.NET 9 Blazor New Features** + +- https://abp.io/community/articles/asp.net-core-blazor-9.0-new-features-summary--x0fovych + +**.NET 9 Performance Improvements Summary** + +- https://abp.io/community/articles/.net-9-performance-improvements-summary-gmww3gl8 + +**What’s new in .NET 9 (Microsoft’s post)** + +- https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview + + + +--- + + + +## We Are Eating Our Own Dog Food + +Before we release any version of ABP, **we test our upcoming version** on our sample apps and live website https://abp.io. The ABP.io website is also built on top of the ABP Framework, and you can see that we have already started to use .NET 9-rc.2 on our live website. + +![Eating our own dog food](dog-food.png) + + + +--- + + + +## Microsoft .NET Support Policy + +Lastly, I want to mention Microsoft's .NET support policy. + +- **.NET 7** support has been **finished** on **May 2024**. +- **.NET 8** will be supported until **November 2026**. +- **.NET 9** is on the standard term support, which means Microsoft will release patches until **May 2026**. + +Find detailed information about the .NET support policy at [this link.](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) + +![.NET Core Official Support Policy](net-support-policy.png) + +--- + + + +## Finally + +.NET 9 is making a significant impact. It introduces features like Native AOT for faster applications, enhanced AI integration and improved tools for cloud-native and cross-platform development, all aimed at simplifying developers’ work. Whether you’re handling small projects or large-scale enterprise applications, it offers enhancements that **elevate your productivity by just upgrading your .NET version to 9.0** diff --git a/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/POST.md b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/POST.md new file mode 100644 index 0000000000..3cd4bdc706 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/POST.md @@ -0,0 +1,125 @@ +# Hybrid Cache in .NET 9 + +.NET 9 introduces an exciting feature: **HybridCache**, an advanced caching mechanism that seamlessly combines multiple caching strategies to maximize performance and scalability. + +It offers a flexible caching solution that combines the best aspects of local and distributed caching. **HybridCache** is particularly useful in scenarios where quick, in-memory access is desirable but data consistency across multiple application instances is also a requirement. + +In this article, we’ll explore **HybridCache** in .NET 9 and how it integrates with ABP Framework using `AbpHybridCache`. This new feature offers a robust solution for applications that need to scale while maintaining efficient caching strategies. + +## What is HybridCache? + +**HybridCache** is designed to merge different caching layers, commonly including an in-memory cache (for high-speed access) and a distributed cache (for scalability across multiple instances). This hybrid approach allows for: + +* **Improved Performance**: Frequently accessed data is stored in-memory, reducing latency. +* **Increased Scalability**: Cached data can still be shared across distributed environments, essential for load-balanced applications. +* **Automatic Synchronization**: Changes in distributed cache automatically update the in-memory cache, ensuring data consistency. + +## Using HybridCache with ABP + +> For more information about the implementation in the ABP side, you can refer to the pull request [here](https://github.com/abpframework/abp/pull/20859). + +ABP's support for **HybridCache** is available starting from version 9.0 through the [`AbpHybridCache`](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs) implementation. By leveraging this feature, developers using ABP can implement hybrid caching in a way that aligns with ABP’s modular and extensible architecture. + +To demonstrate how to use **HybridCache** in ABP, let's start with a simple example. + +> You can create an ABP-based application with v9.0+, and then follow the next steps for using hybrid caching in your application. + +### Configuring the `AbpHybridCacheOptions` (Optional) + +First, you can configure the hybrid cache options in your module class as below (it's optional): + +```csharp +using Microsoft.Extensions.Caching.Hybrid; +using Volo.Abp.Caching.Hybrid; + +public class YourModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //... + + Configure(options => + { + //configuring the global hybrid cache options + options.GlobalHybridCacheEntryOptions = new HybridCacheEntryOptions() + { + Expiration = TimeSpan.FromMinutes(20), + LocalCacheExpiration = TimeSpan.FromMinutes(10) + }; + }); + } +} +``` + +* You can configure the `AbpHybridCacheOptions` to set *keyPrefix* for your cache keys, throw or hide exceptions for the distributed cache (by default *it hides errors*), or configure cache for specific cache item keys and more... +* By setting the `GlobalHybridCacheEntryOptions`, you specify the caching options globally in your application. Thanks to that, you don't need to manually pass the related options whenever you use the `IHybridCache` service. + +### Using the `IHybridCache` Service + +After the configuration, now you can inject the `IHybridCache` and use it to set and retrieve cache values: + +```csharp +using Volo.Abp.Caching.Hybrid; + +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IHybridCache _hybridCache; + + public BookAppService(IHybridCache hybridCache) + { + _hybridCache = hybridCache; + } + + public async Task GetBookWithPageCountAsync(string name) + { + var cacheKey = "cacheKey:book-" + name; + + // Retrieve data from hybrid cache + return await _hybridCache.GetOrCreateAsync(cacheKey, async () => + { + // Simulating getting and returning the data if not exist in the cache + return new BookCacheItem + { + Name = name, + PageCount = 100 + }; + }); + } +} + +public class BookCacheItem +{ + public string Name { get; set; } + + public int PageCount { get; set; } +} +``` + +* You can use the `IHybridCache` or `IHybridCache` service to leverage the hybrid caching. If you use `IHybridCache`as the service, then you should pass the cache key as *string* like in the example above. +* In this example, you used the `GetOrCreateAsync` method, which first tries to get the cache item with the provided cache key, if there is no cache with the specified key, then it runs the factory method and add the returned data to the cache. +* Alternatively, you can use the `SetAsync` method to set the cache item. + +### Debugging the `IHybridCache` Service (deep-dive) + +When you debug the `IHybridCache` service, you'll notice the L1 and L2 cache stores. (L1 is in-memory cache store and L2 is the distributed cache store): + +![](debug-hybrid-cache.png) + +As you can see from the figure, it only set the cache item to the **LocalCache** (`MemoryCache`) and did not set the **BackendCache** (`DistributedCache`) because I did not configure the distributed cache and not running my application in multiple instances. But as you can notice, even without an `IDistributedCache` configuration, the `HybridCache` service will still provide in-process caching. + +**Note:** If you configure distributed caching options, `HybridCache` service uses the distributed cache and sets the **BackendCache**. + +## Conclusion + +The **HybridCache** library in .NET 9 provides a powerful tool for applications needing both high-speed caching and consistency in distributed environments. + +With ABP Framework’s `AbpHybridCache` support, integrating this feature into an ABP-based application becomes straightforward. This setup helps ensure that cached data remains synchronized across instances, bringing a new level of flexibility to caching in .NET 9 applications. + +> For more information, you can refer to the [Microsoft's official document](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0#new-hybridcache-library). + +## References + +- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0#new-hybridcache-library +- https://www.youtube.com/watch?v=TDyZc11cJfA +- https://github.com/abpframework/abp/pull/20803 +- https://github.com/abpframework/abp/pull/20859 diff --git a/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/cover-image.png b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/cover-image.png new file mode 100644 index 0000000000..3494a01265 Binary files /dev/null and b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/cover-image.png differ diff --git a/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/debug-hybrid-cache.png b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/debug-hybrid-cache.png new file mode 100644 index 0000000000..3f8c8e0b1c Binary files /dev/null and b/docs/en/Community-Articles/2024-11-01-Hybrid-Cache-Net-9/debug-hybrid-cache.png differ diff --git a/docs/en/Community-Articles/2024-11-04-EF Core 9 Read-only-Primitive-Collections/POST.md b/docs/en/Community-Articles/2024-11-04-EF Core 9 Read-only-Primitive-Collections/POST.md new file mode 100644 index 0000000000..94d2e7fa4a --- /dev/null +++ b/docs/en/Community-Articles/2024-11-04-EF Core 9 Read-only-Primitive-Collections/POST.md @@ -0,0 +1,86 @@ +# EF Core 9 Read-only Primitive Collections + +In this article, we will explore the new features introduced in EF Core 9, specifically focusing on Read-only Primitive Collections. EF Core 8 introduced support for mapping arrays and mutable lists of primitive types, and you can read more about it [here](https://abp.io/community/articles/ef-core-8-primitive-collections-ttn5b6xp). This has been expanded in EF Core 9 to include read-only collections/lists. Specifically, EF Core 9 supports collections typed as `IReadOnlyList`, `IReadOnlyCollection`, or `ReadOnlyCollection`. + +## Introduction to EF Core 9 Read-only Primitive Collections + +Entity Framework Core 9 introduces several enhancements, one of which is the support for Read-only Primitive Collections. This feature aims to provide better support for scenarios where collections of primitive types, such as `int`, `string`, or `bool`, need to be used in a read-only manner in your entity classes. Previously, developers had to use complex workarounds to ensure collections couldn't be modified, but EF Core 9 now provides a simpler, built-in solution to handle this more effectively. + +### Why Read-only Primitive Collections Matter + +Read-only Primitive Collections are particularly useful when you need to guarantee the integrity of certain data within your entities. For example, imagine you have a `Car` entity that has a collection of `Colors`, represented as a set of enums. You might not want these colors to be modified after they're initially set, ensuring that any business logic reliant on these values remains consistent. + +EF Core 9 introduces a convenient way to define these collections as read-only, helping developers maintain stricter control over their data. + +### How It Works + +Defining a read-only primitive collection is quite straightforward in EF Core 9. You can use the `IReadOnlyList`, `IReadOnlyCollection`, or `ReadOnlyCollection` types to declare your properties, ensuring a consistent read-only behavior. This helps maintain data integrity by preventing modifications after the collection is set. Below is an example that includes a `Car` class and a `Color` enum. The `Car` class has a `Colors` property that holds a read-only list of available colors, ensuring that these values cannot be modified after being initially set: + +```csharp +public enum Color +{ + Black, + White, + Red, + Blue +} + +public class Car +{ + public int Id { get; set; } + public string Brand { get; set; } + public string Model { get; set; } + public IReadOnlyList Colors { get; private set; } = new List { Color.Black, Color.White }.AsReadOnly(); + + protected Car() + { + /* This constructor is for deserialization / ORM purpose */ + } + + public Car(string brand, string model, IEnumerable colors) + { + Brand = brand; + Model = model; + Colors = colors.ToList().AsReadOnly(); + } +} +``` + +In the example above, `Colors` is defined as a read-only list, preventing any accidental modifications once it is set. This ensures that data integrity is maintained without the need for manual validation. + +To query cars with specific colors, you can use the following example: + +```csharp +var colors = new List { Color.Black, Color.White }; +var cars = await context.Cars + .Where(c => c.Colors.Intersect(colors).Any()) + .ToListAsync(); +``` + +The query selects all cars that have any of the specified colors in their `Colors` collection. + +The SQL result looks like this; as you can see, it sends colors as parameters instead of adding them inline. It also uses the `json_each` function to deserialize on the database side: + +```sql +SELECT "c"."id", + "c"."brand", + "c"."colors", + "c"."model" +FROM "cars" AS "c" +WHERE EXISTS (SELECT 1 + FROM (SELECT "c0"."value" + FROM Json_each("c"."colors") AS "c0" + INTERSECT + SELECT "c1"."value" + FROM Json_each(@__colors_0) AS "c1") AS "i") +``` + +### Conclusion + +Read-only primitive collections make it easier to enforce data integrity by preventing changes to your collection data. This feature helps simplify your code while ensuring that critical parts of your data remain consistent. + +## References + +- https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#read-only-primitive-collections +- https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#primitive-collections +- https://abp.io/community/articles/ef-core-8-primitive-collections-ttn5b6xp diff --git a/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/Post.md b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/Post.md new file mode 100644 index 0000000000..174aca756d --- /dev/null +++ b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/Post.md @@ -0,0 +1,54 @@ +# .NET Aspire 9.0 Features + +.NET Aspire 9.0 is the next major release, supporting both .NET 8 and .NET 9. This version includes new features and improvements. + +## Upgrade to .NET Aspire + +Now, you don't need workloads to develop .NET Aspire applications. In your project, you can add an SDK reference to `Aspire.AppHost.Sdk`. +For more information, you can check out [https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/dotnet-aspire-9?tabs=windows#upgrade-to-net-aspire-9](https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/dotnet-aspire-9?tabs=windows#upgrade-to-net-aspire-9) which explains upgrading an existing project in details. + +## Dashboard + +.NET Aspire offers a nice dashboard for developers to observe the performance and behavior of their applications. In this version, there are some enhancements; + +* **Manage resource lifecycle**: You can stop, start, and restart resources. +* **Mobile and responsive support**: The .NET Aspire dashboard is now mobile-friendly. +* **Sensitive properties**: Properties can be marked as sensitive, automatically masking them in the dashboard UI. +* **Volumes**: Configured container volumes are listed in resource details. +* **Health checks**: .NET Aspire 9 adds support for health checks. + +![Resource Lifecycle](./aspire_resource_lifecycle.jpg) + +## Telemetry + +.NET Aspire 9 comes with many new features to the Telemetry service. + +* **Improve telemetry filtering**: Telemetry data can now be filtered by attribute values. +* **Combine telemetry from multiple resources**: If a resource has multiple replicas, you can now filter telemetry data to view from all instances. +* **Browser telemetry support**: The dashboard now supports OpenTelemetry Protocol (OTLP) over HTTP and cross-origin resource sharing (CORS). + +![Telemetry Filtering](./aspire_trace_filter.jpg) + +## Orchestration + +The .NET App Host is a core component of the .NET runtime that helps launch and execute .NET applications. +.NET Aspire 9 introduces many new features to the app host. Let's take a look; + +* **Waiting for dependencies**: You can configure a resource to wait for another resource to start before starting. +* **Resource health checks**: The `Waiting for dependencies` feature uses health checks to determine if a resource is ready. + +## Integrations + +.NET Aspire has integrations with some services and tools that make it easy to get started. New integrations are coming with .NET Aspire 9. + +* Redis Insight +* OpenAI (Preview) +* MongoDB +* Azure + +For Azure part, it is better to check the official documentation here [https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/dotnet-aspire-9-release-candidate-1?tabs=windows&pivots=visual-studio#azure](https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/dotnet-aspire-9-release-candidate-1?tabs=windows&pivots=visual-studio#azure) because it has a very detailed explanation. + +## ABP Studio + +.NET Aspire and [ABP Studio](https://abp.io/studio) are tools for different purposes with different scopes, and they have different approaches to solving problems; many developers may still be confused since they also have some similar functionalities and solve some common problems. You can check the comparison of .NET Aspire and ABP Studio in this [article](https://abp.io/community/articles/.net-aspire-vs-abp-studio-side-by-side-t1c73d1l). + diff --git a/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_resource_lifecycle.jpg b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_resource_lifecycle.jpg new file mode 100644 index 0000000000..011a705beb Binary files /dev/null and b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_resource_lifecycle.jpg differ diff --git a/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_trace_filter.jpg b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_trace_filter.jpg new file mode 100644 index 0000000000..f7563e632b Binary files /dev/null and b/docs/en/Community-Articles/2024-11-05-.NET Aspire 9.0 Features/aspire_trace_filter.jpg differ diff --git a/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/POST.md b/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/POST.md new file mode 100644 index 0000000000..c43c30d268 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/POST.md @@ -0,0 +1,113 @@ +# SignalR supports trimming and Native AOT + +## What is SignalR? + +SignalR is a library that allows you to add real-time web functionality to your applications. It provides a simple API for creating server-to-client remote procedure calls (RPC) that can be called from the server and client. Now SignalR supports trimming and Native AOT in .NET 8.0 and .NET 9.0. You can learn more about [SignalR new features](https://abp.io/community/articles/asp.net-core-signalr-new-features-summary-kcydtdgq) in this article. + +## What is trimming and Native AOT? + +AOT (Ahead-of-Time) compilation is a feature that allows you to compile your application into native code before running it. This can help improve performance and reduce startup times. Trimming is a feature that allows you to remove unused code from your application, reducing its size and improving performance. You can learn more about [Native AOT Compilation](https://abp.io/community/articles/native-aot-compilation-in-.net-8-oq7qtwov) in this article. + +## How to use SignalR with trimming and Native AOT? + +You can create ASP.NET Core AOT application with using the following command: + +```bash +dotnet new webapiaot -n Acme.Sample +``` + +The created application uses `CreateSlimBuilder` method to create minimal builder for the application. You can use `CreateBuilder` method to create a builder with all the services registered. However, deploying an application with `CreateSlimBuilder` method is more convenient because it reduces the size of the application. You can learn more about [CreateSlimBuilder vs CreateBuilder](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot#createslimbuilder-vs-createbuilder). + +Replace the `Program.cs` file with the following code: + +```csharp +using Microsoft.AspNetCore.SignalR; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateSlimBuilder(args); + +builder.Services.AddSignalR(); +builder.Services.Configure(o => +{ + o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +var app = builder.Build(); + +app.MapHub("/chatHub"); +app.MapGet("/", () => Results.Content(""" + + + + SignalR Chat + + + + + +
    + + + + + +""", "text/html")); + +app.Run(); + +[JsonSerializable(typeof(string))] +internal partial class AppJsonSerializerContext : JsonSerializerContext { } + +public class ChatHub : Hub +{ + public async Task SendMessage(string user, string message) + { + await Clients.All.SendAsync("ReceiveMessage", user, message); + } +} +``` + +It is a simple chat application that uses SignalR to send and receive messages. + +![chat](chat.png) + +Before deploying the application, ensure that **Desktop development with C++** is installed on your machine if you're using Windows OS. For more details, you can check the [pre-requisites](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot#prerequisites). + +You can deploy the application with the following command: + +```bash +dotnet publish -c Release +``` + +### Limitations + +Since we are using Native AOT, there are some limitations that you should be aware of: + +- **Only the JSON protocol is supported**: For the payload serialization in SignalR, only the JSON protocol is supported. You need to configure the `JsonHubProtocolOptions` to use the `AppJsonSerializerContext` for serialization/deserialization. +- **Reflection**: Native AOT does not support reflection. You need to use the `JsonSerializable` attribute to specify the types that should be serialized/deserialized. In this example, we have used the `JsonSerializable` attribute for the `string` type in the `AppJsonSerializerContext` class. + +For more details, you can check the [limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot#limitations-of-native-aot-deployment) of Native AOT. + +## Conclusion + +In this article, we learned how to use SignalR with trimming and Native AOT in .NET 8.0 and .NET 9.0. We created a simple chat application that uses SignalR to send and receive messages. We also discussed the limitations of using Native AOT and how to overcome them. + +For more information, you can refer to the [Microsoft's official document](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0#signalr-supports-trimming-and-native-aot). \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/chat.png b/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/chat.png new file mode 100644 index 0000000000..d6dff70d6d Binary files /dev/null and b/docs/en/Community-Articles/2024-11-05-SignalR-supports-trimming-and-Native-AOT/chat.png differ diff --git a/docs/en/Community-Articles/2024-11-06-Keyed-DI-in-Middlewares-Net-9/post.md b/docs/en/Community-Articles/2024-11-06-Keyed-DI-in-Middlewares-Net-9/post.md new file mode 100644 index 0000000000..4cb6992d0d --- /dev/null +++ b/docs/en/Community-Articles/2024-11-06-Keyed-DI-in-Middlewares-Net-9/post.md @@ -0,0 +1,58 @@ +# Middleware Now Supports Keyed Dependency Injection in .NET 9 + +This article explores a new feature in .NET 9 that enables keyed dependency injection in middleware. Previously, .NET 8 introduced keyed services, which allowed developers to register multiple instances of the same service type with distinct keys. Now, .NET 9 extends this feature to middleware, making it easier to inject specific services within the middleware based on defined keys. For more details, see this [overview on the .NET blog](https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/rc1/aspnetcore.md#keyed-di-in-middleware). + +## What is Keyed Dependency Injection? + +Keyed dependency injection is a technique for registering multiple service versions with unique identifiers, or “keys.” This approach is especially helpful when multiple implementations of the same service are required in different contexts. For example, you may have various logging services but want to inject a specific logger based on the application’s current needs. By using keys, developers can ensure that the appropriate service version is injected precisely where it’s needed. + +## Using Keyed Dependency Injection in Middleware + +In .NET 9, developers can now use keyed dependency injection directly in middleware. Keyed services can be injected through the middleware constructor or via the `Invoke`/`InvokeAsync` methods, allowing for straightforward and flexible control of service instances in middleware components. Here’s an example of how to configure and use keyed dependency injection in middleware: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Register services with unique keys +builder.Services.AddKeyedSingleton("test"); +builder.Services.AddKeyedScoped("test2"); + +var app = builder.Build(); +app.UseMiddleware(); +app.Run(); + +internal class MyMiddleware +{ + private readonly RequestDelegate _next; + private readonly MySingletonClass _singletonService; + + // Constructor injection with key + public MyMiddleware(RequestDelegate next, [FromKeyedServices("test")] MySingletonClass singletonService) + { + _next = next; + _singletonService = singletonService; + } + + // Invoke method with additional scoped service injection using key + public Task Invoke(HttpContext context, [FromKeyedServices("test2")] MyScopedClass scopedService) + { + // Middleware logic here + return _next(context); + } +} +``` + +In this example: +- `MySingletonClass` and `MyScopedClass` are registered with unique keys (`"test"` and `"test2"`). +- These services are injected into the middleware through both the constructor and `Invoke` method, based on their respective keys. + +This approach allows developers to manage which service instances are available within middleware precisely. + +## Conclusion + +Keyed dependency injection in middleware is a significant addition in .NET 9. It provides developers with more control over which services are injected based on specific keys. This enhancement enables selective service injection in middleware scenarios, allowing for more modular and maintainable applications. + +## References + +- [.NET 9 Release Notes](https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/rc1/aspnetcore.md#keyed-di-in-middleware) +- [Dependency Injection and Keyed Services](https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection#keyed-services) \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-11-06-Optimize-static-web-asset-delivery/POST.md b/docs/en/Community-Articles/2024-11-06-Optimize-static-web-asset-delivery/POST.md new file mode 100644 index 0000000000..67c4762952 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-06-Optimize-static-web-asset-delivery/POST.md @@ -0,0 +1,108 @@ +# Optimizing Static Asset Delivery feature in ASP.NET Core 9.0 + +Delivering static assets efficiently is a key factor in building performant web applications. By optimizing how assets like CSS, JavaScript, and images are served to the browser, you can reduce load times, decrease network traffic, and improve the overall user experience. + +One powerful tool to help achieve this is **MapStaticAssets**, a feature in ASP.NET Core that significantly optimizes the delivery of static resources. Whether you're working with Blazor, Razor Pages, MVC, or other UI frameworks, **MapStaticAssets** streamlines asset management and ensures that your web app delivers resources in the most efficient way possible. + +## Why Optimizing Static Assets Matters + +Serving static assets without optimization can lead to several performance bottlenecks: + +- **Excessive network requests**: The browser may need to request the same resources multiple times, even if they haven’t changed. +- **Unnecessary data transfer**: Larger files are sent over the network, consuming bandwidth and slowing down page loads. +- **Outdated assets**: Without proper cache management, users may receive stale versions of files after an app update. + +Optimizing static assets involves compressing files, managing caching headers, and ensuring that only the necessary resources are sent to the client. **MapStaticAssets** takes care of all these issues in a seamless, automated way. + +## What is MapStaticAssets? + +**MapStaticAssets** is designed to enhance the default static asset serving mechanism in ASP.NET Core. It can replace `UseStaticFiles` in most scenarios and comes with several built-in optimizations. These optimizations are executed at both build and publish time, ensuring that static resources are served in the most efficient way possible when your app is running. + +Here's how you can implement **MapStaticAssets** in your app: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseRouting(); + +app.UseAuthorization(); + +// Replacing UseStaticFiles with MapStaticAssets +app.MapStaticAssets(); +app.MapRazorPages(); + +app.Run(); +``` + +## Key Features of MapStaticAssets + +1. **Build-time Compression**: + **MapStaticAssets** automatically compresses all static assets during the build process. It uses **gzip** compression during development and **gzip + brotli** compression when publishing. This reduces the file size significantly, ensuring faster download times. + + For example, in a default Razor Pages template, assets like `bootstrap.min.css` and `jquery.js` are compressed by over 80%, resulting in significantly reduced file sizes: + + | File | Original Size | Compressed Size | Compression Reduction | + |----------------------|---------------|-----------------|-----------------------| + | `bootstrap.min.css` | 163 KB | 17.5 KB | 89.26% | + | `jquery.js` | 89.6 KB | 28 KB | 68.75% | + | `bootstrap.min.js` | 78.5 KB | 20 KB | 74.52% | + | **Total** | 331.1 KB | 65.5 KB | 80.20% | + +2. **Content-based ETags**: + **MapStaticAssets** generates **ETags** based on the SHA-256 hash of the file content, encoded in Base64. This ensures that the browser only re-downloads a resource if its content has changed. This eliminates unnecessary network requests, improving page load speeds. + +3. **Smaller File Sizes for Libraries**: + Popular component libraries, such as **Fluent UI Blazor** and **MudBlazor**, benefit from similar compression optimizations. For example, the size of the **MudBlazor** library is reduced by over 90%, from 588 KB to just 46.7 KB after compression. + + | File | Original Size | Compressed Size | Compression Reduction | + |----------------------|---------------|-----------------|-----------------------| + | `MudBlazor.min.css` | 541 KB | 37.5 KB | 93.07% | + | `MudBlazor.min.js` | 47.4 KB | 9.2 KB | 80.59% | + | **Total** | 588.4 KB | 46.7 KB | 92.07% | + +4. **Automatic Optimization**: + As libraries or components are added or updated, **MapStaticAssets** automatically optimizes the assets as part of the build process. This includes minimizing the size of JavaScript and CSS files, reducing the impact of mobile or low-bandwidth environments. + +5. **Serving Assets with a CDN**: + Although **MapStaticAssets** is focused on server-side optimizations, integrating a **CDN (Content Delivery Network)** can further boost performance by serving static assets from servers geographically closer to the user, reducing latency. + +## Comparing MapStaticAssets to IIS Dynamic Compression + +**MapStaticAssets** provides several advantages over traditional dynamic compression techniques, such as IIS **gzip** compression: + +- **Simplicity**: There is no need for server-specific configuration, making **MapStaticAssets** easy to implement. +- **Performance**: By compressing assets at build time, the app doesn't need to perform compression during every request, which improves server performance. +- **Optimization**: Developers can focus on ensuring that assets are compressed to the smallest possible size during the build process. + +For example, using **MapStaticAssets**, a file like `MudBlazor.min.css` is compressed down to 37.5 KB, whereas IIS dynamic compression might result in a size of 90 KB. This represents a **59%** reduction in size. + +## About MapAbpStaticAssets + +The ABP framework is 100% compatible with this new feature. + +However, some JavaScript, CSS, and image files exist in the [Virtual File System](https://abp.io/docs/latest/framework/infrastructure/virtual-file-system), which ASP.NET Core's **MapStaticAssets** can't handle. For these files, additional **StaticFileMiddleware** is needed to serve them, which is where **MapAbpStaticAssets** comes in. + +**MapAbpStaticAssets** adds the necessary **StaticFileMiddleware** to ensure that virtual files are correctly served. This middleware setup ensures seamless delivery of virtual resources alongside static assets. + +You can view the source code of **MapAbpStaticAssets** on [GitHub](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs#L129-L198). + +## Conclusion + +Optimizing static asset delivery is essential for building fast, efficient web applications. **MapStaticAssets** simplifies and automates the optimization of static files by providing build-time compression, caching headers, and content-based ETags. This ensures that your app's static assets are always delivered in the most efficient way, whether users are on fast broadband or slower mobile connections. By using **MapStaticAssets**, you can deliver a faster, more reliable experience for your users with minimal effort. + +## References + +* [Static files in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-9.0) +* [What's new in ASP.NET Core 9.0](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#optimize-static-web-asset-delivery) diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/cover.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/cover.png new file mode 100644 index 0000000000..4ac79513c3 Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/cover.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img1.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img1.png new file mode 100644 index 0000000000..e47683afd8 Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img1.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img2.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img2.png new file mode 100644 index 0000000000..a89415db1d Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img2.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img3.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img3.png new file mode 100644 index 0000000000..8698b153ab Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img3.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img4.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img4.png new file mode 100644 index 0000000000..30e539ffb2 Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img4.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img5.png b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img5.png new file mode 100644 index 0000000000..5b5c1a9b7d Binary files /dev/null and b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/img5.png differ diff --git a/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/post.md b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/post.md new file mode 100644 index 0000000000..3e950c38c0 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-13-BuiltIn-OpenApi-Documentation/post.md @@ -0,0 +1,164 @@ +Built-in OpenAPI Document Generation with .NET 9 — No more SwaggerUI! 👋 +======================================================================== + +![Cover](cover.png) + +What’s Swagger UI? +------------------ + +[Swagger UI](https://swagger.io/) is an open-source tool that automatically generates an interactive, web-based documentation interface for WebAPIs. +It supports OpenAPI standards. It was very popular tool among the ASP.NET Core developers from 2020 to 2024. +Because it was a built-in tool comes with ASP.NET Core default templates. +We liked this tool because it was the first tool that allows us to make WebAPI calls for testing. +Now it provides paid services as well as free ones. + +> Previously, Swagger was included by default from **.NET 5** to **.NET 8** in .NET web templates. + +--- + + + +What’s OpenAPI? +--------------- + +OpenAPI is a standard specification for defining REST APIs. +The official website is [https://www.openapis.org/](https://www.openapis.org/). +Microsoft is now using OpenAPI and here is the official documentation 👉 [https://aka.ms/aspnet/openapi](https://aka.ms/aspnet/openapi) + +--- + + + + + +Replacement of Swagger UI with OpenAPI +---------------------------------------------------------------------------- + +Swagger UI is no longer integrated into NET 9, as Microsoft wants a solution with first-class support, better control, and enhanced security. As you see in the below screenshot, Microsoft declares that it's already removed. + +![Docs](img2.png) + +--- + + + + + +## Why is Swagger Removed from .NET 9? + +In March 2024, the ASP.NET Core team announced that they are removing the `Swashbuckle.AspNetCore` dependency from web templates from .NET 9 release. + + + +> This decision was influenced by the project's lack of active maintenance and the absence of an official release for .NET 8. + + + +Microsoft team created a new package `Microsoft.AspNetCore.OpenApi`. It provides built-in OpenAPI document generation just like Swagger. So Microsoft doesn't depend on external tools. Because in every .NET release, they need to ask the owners of the external tool libraries to align with their new version. And sometimes these library owners cannot update their code-base according to the recent .NET changes. And it is becoming harder for Microsoft to support the 3rd party libraries under these circumstances. Basically reducing 3rd party dependencies will help Microsoft fast release cycles. + +I read Reddit, GitHub discussions and YouTube reviews about this topic. As I see community members expressed concerns about the inactivity of Swashbuckle and they are discussing alternatives like contributing to or forking the project. The Microsoft team also contacted the owners of Swashbuckle and NSwag to explore potential collaborations and ensure a smooth transition for developers. + +In the below GitHub issue, you can see the details of this decision: + +* [github.com/dotnet/aspnetcore/issues/54599](https://github.com/dotnet/aspnetcore/issues/54599) + + + +**Jeremy** -Product Manager- at Microsoft, answers why they took this decision in [this post](https://github.com/dotnet/aspnetcore/issues/54599#issuecomment-2004975574). + +![Jeremy Comments](img3.png) + +As a summary; + +**The change is due to a lack of maintenance of the Swagger library**, although it has seen some recent updates. This aims to reduce dependency on external tools and provide a streamlined, out-of-the-box experience for generating OpenAPI documentation for ASP.NET Core Web APIs. + +--- + + + + + +What are the Benefits of the New OpenAI Package? +--------------------------------------------------------------- + +### Native Support and Reduced Dependency + +The new `Microsoft.AspNetCore.OpenApi` package provides first-class citizen support for OpenAPI. It reduces reliance on external tools like Swashbuckle or NSwag for basic documentation needs. The native implementation leverages source generators to reduce runtime overhead. + +### Simplified Configuration + +No need extra setup or 3rd party integrations. Just by defining controllers and endpoints, ASP.NET Core automatically generates OpenAPI specifications. + +### Well Integration with Minimal APIs + +[Minimal APIs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis) introduced in .NET 6. There's an optimized built-in support for Minimal APIs. It automatically adds metadata for routes, request parameters, and responses. + +### Compatibility with Existing Tools + +You can still use the output of OpenAPI with Swagger or NSwag... So it doesn't mean that in this case you have only one option when you use OpenAPI. + +--- + + + +How to Use the New OpenAPI in .NET9? +------------------------------------ + +When you create a new ASP.NET Core project, you can see the below checkbox to add OpenAPI. + +![New .NET 9 Project Screen](img5.png) + +I created a new .NET 9 web project, I saw that OpenAPI had already been added. + +![Package Reference](img4.png) + +## Add OpenAPI Support For Your Existing Project + +Upgrade your project to .NET 9 and add the required NuGet package [Microsoft.AspNetCore.OpenApi](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi) + +``` +dotnet add package Microsoft.AspNetCore.OpenApi +``` + +### + +Add the following services and middleware in `Program.cs` + +``` +var builder = WebApplication.CreateBuilder(); +builder.Services.AddOpenApi(); //<<----- +var app = builder.Build(); +app.MapOpenApi(); //<<----- +app.MapGet("/", () => "Test"); +app.Run(); +``` + +Your OpenAPI document URL is [_https://localhost:7077/openapi/v1.json_](https://localhost:7077/openapi/v1.json) + +Change the port to your active port. This is how it looks like: + +![Web UI of the Documentation](img1.png) + +--- + + + +Alternative 3rd Party Tool: Scalar +================================== + +**Scalar** is an open-source API platform for RestAPI documentation. Also, it provides an interface for interacting with RESTful API. Generates interactive and user-friendly API documentation. Supports OpenAPI and Swagger specifications. It’s open-source with **7K stars** on GitHub. + +See the repo 👉 [https://github.com/scalar/scalar](https://github.com/scalar/scalar). + + + +That's all from the replacement of Swagger in .NET 9. +Happy coding 👨‍💻 + + + +**References** + +* [https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#openapi](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#openapi) + + diff --git a/docs/en/Community-Articles/2024-11-14-Csharp-13-Features/Post.md b/docs/en/Community-Articles/2024-11-14-Csharp-13-Features/Post.md new file mode 100644 index 0000000000..12a1dea431 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-14-Csharp-13-Features/Post.md @@ -0,0 +1,202 @@ +# C# 13 Features + +C# 13 is the latest version of C# and it comes with a lot of new features. In this article, we will discuss some of the new features of C# 13. + +## `params` collections + +With the C# 13, method parameter with `params` keyword isn't limited to be an array. You can now use any collection type that implements `IEnumerable` interface. + +Let's see how it can help us in our code. + +```csharp +public IEnumerable GetOdds(params IEnumerable numbers) +{ + foreach (var number in numbers) + { + if (number % 2 != 0) + { + Console.WriteLine(number); + } + } +} +``` + +## New lock object + +I'm sure you have used `lock` statement in your code to synchronize access to a shared resource. With C# 13, you can now use a new lock object that is more efficient than the traditional lock object. +The new `Lock` type provides better thread synchronization through its API. When `Lock.EnterScope()` method is called, it returns a struct named `Scope` that contains a `Dispose` method. The `Dispose` method is called when the `Scope` object goes out of scope, which releases the lock. C# `using` statement recognizes the `Dispose` method and calls it automatically like it does with other `IDisposable` objects. + +It was something similar before: +```csharp +private object _lock = new(); + +public void DoSomething() +{ + lock (_lock) + { + // Do something + } +} +``` + +Now, you can use the new lock object like this: +```csharp +System.Threading.Lock x = new System.Threading.Lock(); +public void DoSomething() +{ + using (x.EnterScope()) + { + // Do something + } +} +``` + +## New escape sequence +In C# 13, a new escape sequence `\e` has been introduced to represent the `ESCAPE` character, Unicode `U+001B`. Previously, you had to use `\u001b` or `\x1b` to represent this character. The new `\e` escape sequence simplifies this process and avoids potential issues with hexadecimal digits following `\x1b`. + +> You can check [here](https://en.wikipedia.org/wiki/ANSI_escape_code#C0_control_codes) for ANSI escape codes. + +## Implicit index access + +The implicit "from the end" index operator, `^`, is now allowed in an object initializer expression. + +It was not possible before, but now you can do this: + +```csharp +var countdown = new TimerRemaining() +{ + buffer = + { + [^1] = 0, + [^2] = 1, + [^3] = 2, + [^4] = 3, + [^5] = 4, + [^6] = 5, + [^7] = 6, + [^8] = 7, + [^9] = 8, + [^10] = 9 + } +}; +``` + +It's a great feature that makes the code more readable and maintainable. Still not a big deal, but it's nice to have it. + +## `ref` and `unsafe` in iterators and async methods + +In C# 13, the restrictions on using `ref` and `unsafe` constructs in iterators and async methods have been relaxed. Previously, you couldn't declare local `ref` variables or use unsafe contexts in these methods. Now, you can declare ref local variables and use unsafe contexts in async methods and iterators, provided they are not accessed across `await` or `yield` boundaries + + +This change allows for more expressive and efficient code, especially when working with types like `System.Span` and `System.ReadOnlySpan`. The compiler ensures that these constructs are used safely, and it will notify you if any safety rules are violated. + +You can read more about this feature on the [Microsoft Learn page](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-13.0/ref-unsafe-in-iterators-async). + + + +## More partial members + +In C# 13, the concept of partial members has been expanded to include partial properties and partial indexers. Previously, only methods could be defined as partial members. This means you can now split the definition of properties and indexers across multiple files, just like you could with methods. + +For example, you can declare a partial property in one part of your class and implement it in another part. Here's a simple illustration: + +```csharp +public partial class MyClass +{ + // Declaring declaration + public partial string MyProperty { get; set; } +} + +public partial class MyClass +{ + // Implementing declaration + private string _myProperty; + public partial string MyProperty + { + get => _myProperty; + set => _myProperty = value; + } +} +``` + +This feature allows for better organization and modularization of your code, especially in large projects where different parts of a class might be implemented by different team members. + +## Overload resolution priority + +What does "Overload resolution priority" section mean in this page? +In C# 13, the OverloadResolutionPriority attribute allows library authors to specify which method overload should be preferred by the compiler when multiple overloads are available. This attribute helps avoid ambiguity and ensures that the most appropriate overload is chosen, even if it might not be the most obvious choice based on traditional overload resolution rules. + +This may be useful in scenarios where you have multiple overloads that are equally valid, but you want to prioritize one over the others. The attribute can be applied to a method or constructor to indicate its priority in the overload resolution process. It can prevent unexpected behavior and make your code more predictable and maintainable. + + +Let me show with an example: +```csharp +public class Example +{ + // Existing method + public void Display(string message = "Hello!") + { + Console.WriteLine("Message: " + message); + } + + // New, more efficient method with higher priority + [OverloadResolutionPriority(1)] + public void Display(string message = "Hello!", int repeatCount = 3) + { + for (int i = 0; i < repeatCount; i++) + { + Console.WriteLine("Message: " + message); + } + } +} + +class Program +{ + static void Main() + { + Example example = new Example(); + + // Normally, you can't compile this code because of ambiguity: + example.Display(); + } +} +``` + +Output: +``` +Message: Hello! +Message: Hello! +Message: Hello! +``` + +## The `field` keyword + +n C# 13, the `field` keyword is introduced as a preview feature to simplify property accessors. This keyword allows you to reference the compiler-generated backing `field` directly within a property accessor, eliminating the need to declare an explicit backing `field` in your type declaration. + +For example, instead of writing: + +```csharp +private int _value; +public int Value +{ + get => _value; + set => _value = value; +} +``` + +You can now write: + +```csharp +public int Value +{ + get => field; + set => field = value; +} +``` + +This makes your code cleaner and more concise. However, be cautious if you have a `field` named `field` in your class, as it could cause confusion. You can disambiguate by using `@field` or `this.field`. + +Make sure you're using the latest `LangVersion` in your `.csproj` project file to enable this feature. +```xml +preview +``` \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-11-14-EF-Core-9-Linq-SQL-Translation/POST.md b/docs/en/Community-Articles/2024-11-14-EF-Core-9-Linq-SQL-Translation/POST.md new file mode 100644 index 0000000000..2e91d30e7f --- /dev/null +++ b/docs/en/Community-Articles/2024-11-14-EF-Core-9-Linq-SQL-Translation/POST.md @@ -0,0 +1,165 @@ +# EF Core 9 LINQ & SQL translation + +EF Core improves the translation of LINQ queries to SQL with every release. EF Core 9 is no exception. This article will show you some of the improvements in EF Core 9. + +EF Core 9 includes a lot of improvements in LINQ to SQL translation. we don't cover all of them in this article. You can find more information in the [official release notes](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#linq-and-sql-translation). + +## Support for complex types + +### GroupBy + +EF Core now supports grouping by complex type instance. For example: + +```csharp +var groupedAddress = await context.Customers + .GroupBy(c => new { c.Address }) + .Select(g => new { g.Key, Count = g.Count() }) + .ToListAsync(); +``` + +Address is a complex type as a value object here. + +### ExecuteUpdate + +EF Core now supports updating a complex type. For example: + +```csharp +var newAddress = new Address("New Street", "New City", "New Country"); + +await context.Customers + .Where(e => e.Region == "Turkey") + .ExecuteUpdateAsync(s => s.SetProperty(b => b.Address, newAddress)); +``` + +EF Core updates each column of the complex type. + +## Prune unneeded elements from SQL + +Ef Core now translates LINQ queries to SQL more efficiently. It will remove unneeded elements from the SQL query and bring better performance. + +### Table pruning + +When you use table-per-hierarchy (TPH) inheritance, previously EF Core generated SQL queries that included JIONs to tables that were not needed. + +For example: + +```csharp +public class Order +{ + public int Id { get; set; } + ... + + public Customer Customer { get; set; } +} + +public class DiscountedOrder : Order +{ + public double Discount { get; set; } +} + +public class Customer +{ + public int Id { get; set; } + ... + + public List Orders { get; set; } +} + +public class AppContext : DbContext +{ + ... + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTptMappingStrategy(); + } +} +``` + +Consider the following query to get all customers with at least one order: + +```csharp +var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync(); +``` + +Previously, EF Core generated the following SQL query: + +```sql +SELECT [c].[Id], [c].[Name] +FROM [Customers] AS [c] +WHERE EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id] + WHERE [c].[Id] = [o].[CustomerId]) +``` + +It included a JOIN to the `DiscountedOrders` table, which was not needed. In EF Core 9, the generated SQL query is: + +```sql +SELECT [c].[Id], [c].[Name] +FROM [Customers] AS [c] +WHERE EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[Id] = [o].[CustomerId]) +``` + +## EF Core in ABP + +ABP Framework is built on top of the latest technologies. It will support EF Core 9 as soon as it is released. You can use the latest features of EF Core in your ABP applications. + +For example, you can use the `ExecuteUpdateAsync` method in your ABP application: + +```csharp +public class Book : FullAuditedAggregateRoot +{ + public string Name { get; set; } + + public float Price { get; set; } + + public string Author { get; set; } +} + +public class AppContext : AbpDbContext +{ + public DbSet Books { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(builder); + + builder.Entity(b => + { + b.ToTable("Books"); + b.ConfigureByConvention(); + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + b.Property(x => x.Author).IsRequired().HasMaxLength(64); + }); + } +} + +public class BookRepository : EfCoreRepository, IBookRepository +{ + public BookRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task UpdatePriceByAuthorAsync(string author, float price) + { + await (await GetDbSetAsync()) + .Where(b => b.Author == author) + .ExecuteUpdateAsync(b => b.SetProperty(x => x.Price, price)); + } +} +``` + +* `FullAuditedAggregateRoot` is an aggregate root base class with auditing properties provided by ABP Framework. +* `IRepository` is a generic repository interface provided by ABP Framework that provides CRUD operations and you can use EF Core's API in your entity repository implementation. + +## References + +* [LINQ and SQL translation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#linq-and-sql-translation) +* [ABP Entity Framework Core Integration](https://abp.io/docs/latest/framework/data/entity-framework-core) +* [ABP Entities](https://abp.io/docs/latest/framework/architecture/domain-driven-design/entities) diff --git a/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md new file mode 100644 index 0000000000..33dd9eb292 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md @@ -0,0 +1,309 @@ +# ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app + +We have introduced a new feature in the ABP framework to bundle the `JavaScript/CSS` files in the Blazor wasm app. This feature is called `Global Assets`. +With this feature, you don't need to run the `abp bundle` command to manually create/maintain the `global.js` and `global.css` files in your Blazor wasm app. + +## How Global Assets works? + +The new `Blazor wasm app` has two projects: + +1. `MyProjectName` (ASP.NET Core app) +2. `MyProjectName.Client` (Blazor wasm app) + +The `MyProjectName` reference the `MyProjectName.Client` project, and will be the entry point of the application, which means the `MyProjectName` project will be the `host` project of the `MyProjectName.Client` project. + +The static/virtual files of `MyProjectName` can be accessed by the `MyProjectName.Client` project, so we can create dynamic global assets in the `MyProjectName` project and use them in the `MyProjectName.Client` project. + +## How it works in ABP? + +We have created a new package `WebAssembly.Theme.Bundling` for the theme `WebAssembly` module and used the `Volo.Abp.AspNetCore.Mvc.UI.Bundling.BundleContributor` to add `JavaScript/CSS` files to the bundling system. + +* LeptonXLiteTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule` +* LeptonXTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` +* LeptonTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonThemeBundlingModule` +* BasicTheme: `AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule` + +The new `ThemeBundlingModule` only depends on `AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule(new package)`. It's an `abstractions module`, which only depends on `AbpAspNetCoreMvcUiBundlingAbstractionsModule`. + +We will get all `JavaScript/CSS` files on `OnApplicationInitializationAsync` method of `AbpAspNetCoreMvcUiBundlingModule` from bundling system and add them to `IDynamicFileProvider` service. After that, we can access the `JavaScript/CSS` files in the Blazor wasm app. + +## Add the Global Assets in the module + +If your module has `JavaScript/CSS` files that need to the bundling system, You have to create a new project(`YourModuleName.Blazor.WebAssembly.Bundling`) to your module solution, and reference the new project in the `MyProjectName` project and module dependencies. + +The new project should **only** depend on the `AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule` and define `BundleContributor` classes to contribute the `JavaScript/CSS` files. + +> Q: The new project(`YourModuleName.Blazor.WebAssembly.Bundling`) doesn't have the `libs/myscript.js` and `libs/myscript.css` files why the files can be added to the bundling system? + +> A: Because the `MyProjectName.Client` will depend on the `MyBlazorModule(YourModuleName.Blazor)` that contains the `JavaScript/CSS` files, The `MyProjectName` is referencing the `MyProjectName.Client` project, so the `MyProjectName` project can access the `JavaScript/CSS` files in the `MyProjectName.Client` project and add them to the bundling system. + +```csharp +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class MyBlazorWebAssemblyBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Script Bundles + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleScriptContributor)); + + // Style Bundles + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleStyleBundleContributor)); + }); + } +} +``` + +```csharp +public class MyModuleBundleScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.js"); + } +} + +public class MyModuleBundleStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.css"); + } +} +``` + +## Use the Global Assets in the Blazor WASM + +### MyCompanyName.MyProjectName.Blazor + +Convert your `MyCompanyName.MyProjectName.Blazor` project to integrate the `ABP module` system and depend on the `AbpAspNetCoreMvcUiBundlingModule` and `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule`: + +* The `AbpAspNetCoreMvcUiBundlingModule` uses to create the `JavaScript/CSS` files to virtual files. +* The `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` uses to add theme `JavaScript/CSS` to the bundling system. + +Here is how your project files look like: + +**`Program.cs`:** + +```csharp +public class Program +{ + public async static Task Main(string[] args) + { + //... + + var builder = WebApplication.CreateBuilder(args); + builder.Host.AddAppSettingsSecretsJson() + .UseAutofac() + .UseSerilog(); + await builder.AddApplicationAsync(); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); + return 0; + + //... + } +} +``` + +**`MyProjectNameBlazorModule.cs`:** + +```csharp +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcUiBundlingModule), + typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule) //Should be added! +)] +public class MyProjectNameBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //https://github.com/dotnet/aspnetcore/issues/52530 + Configure(options => + { + options.SuppressCheckForUnhandledSecurityMetadata = true; + }); + + // Add services to the container. + context.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var env = context.GetEnvironment(); + var app = context.GetApplicationBuilder(); + + // Configure the HTTP request pipeline. + if (env.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else + { + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.MapAbpStaticAssets(); + app.UseRouting(); + app.UseAntiforgery(); + + app.UseConfiguredEndpoints(builder => + { + builder.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies()); + }); + } +} +``` + +**`MyCompanyName.MyProjectName.Blazor.csproj`:** + +```xml + + + + + + if you're using LeptonXTheme + + +``` + +### BlazorWebAssemblyBundlingModule in the ABP commercial + +Here is the list of `Bundling Modules` in the ABP commercial. If you're using the pro template, you should add them to the `MyCompanyName.MyProjectName.Blazor` project. + +| BundlingModules | Nuget Package | +|---------------------------------------------|-----------------------------------------------------| +| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling | +| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling | +| SaasHostBlazorWebAssemblyBundlingModule | Volo.Saas.Host.Blazor.WebAssembly.Bundling | +| ChatBlazorWebAssemblyBundlingModule | Volo.Chat.Blazor.WebAssembly.Bundling | +| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling | + + +### MyCompanyName.MyProjectName.Blazor.Client + +1. Remove the `global.JavaScript/CSS` files from the `MyCompanyName.MyProjectName.Blazor`'s `wwwroot` folder. +2. Remove the `AbpCli:Bundle` section from the `appsettings.json` file. +3. Remove all BundleContributor classes that inherit from IBundleContributor. Then, create `MyProjectNameStyleBundleContributor` and `MyProjectNameScriptBundleContributor` classes to add your style and JavaScript files. Finally, add them to `AbpBundlingOptions`. + + +```cs +public class MyProjectNameStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.css", true)); + } +} + + +public class MyProjectNameScriptBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.js", true)); + } +} +``` + +```cs +Configure(options => +{ + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor)); + + var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global); + globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor)); +}); +``` + +## Use the Global Assets in the Blazor WebApp + +### MyCompanyName.MyProjectName.Blazor.WebApp + +Depending on the `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` in your `MyCompanyName.MyProjectName.Blazor.WebApp` project. + +* The `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` uses to add theme `JavaScript/CSS` to the bundling system. + + +### BlazorWebAssemblyBundlingModule in the ABP commercial + +Here is the list of `Bundling Modules` in the ABP commercial. If you're using the pro template, you should add them to the `MyCompanyName.MyProjectName.Blazor.WebApp` project. + +| BundlingModules | Nuget Package | +|---------------------------------------------|-----------------------------------------------------| +| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling | +| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling | +| SaasHostBlazorWebAssemblyBundlingModule | Volo.Saas.Host.Blazor.WebAssembly.Bundling | +| ChatBlazorWebAssemblyBundlingModule | Volo.Chat.Blazor.WebAssembly.Bundling | +| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling | + + +### MyCompanyName.MyProjectName.Blazor.WebApp.Client + +1. Remove the `global.JavaScript/CSS` files from the `MyCompanyName.MyProjectName.Blazor.WebApp.Client`'s `wwwroot` folder. +2. Remove the `AbpCli:Bundle` section from the `appsettings.json` file. +3. Remove all BundleContributor classes that inherit from IBundleContributor. Then, create `MyProjectNameStyleBundleContributor` and `MyProjectNameScriptBundleContributor` classes to add your style and JavaScript files. Finally, add them to `AbpBundlingOptions`. + +```cs +public class MyProjectNameStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.css", true)); + } +} + + +public class MyProjectNameScriptBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.js", true)); + } +} +``` + +```cs +Configure(options => +{ + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor)); + + var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global); + globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor)); +}); +``` + +### Check the Global Assets + +Run the `MyProject` project and check the `https://localhost/global.js` and `https://localhost/global.css` files. You should be able to see the `JavaScript/CSS` files content from the Bundling system: + +![global](image.png) + +## GlobalAssets(AbpBundlingGlobalAssetsOptions) + +You can configure the JavaScript and CSS file names in the `GlobalAssets` property of the `AbpBundlingOptions` class. + +The default values are `global.js` and `global.css`. + +## Conclusion + +With the new `Global Assets` feature, you can easily bundle the `JavaScript/CSS` files in the Blazor wasm app. This feature is very useful for the Blazor wasm app, and it will save you a lot of time and effort. We hope you will enjoy this feature and use it in your projects. + +## References + +* [Virtual Files](https://docs.abp.io/en/abp/latest/Virtual-Files) +* [Bundle Contributors](https://abp.io/docs/latest/framework/ui/mvc-razor-pages/bundling-minification#bundle-contributors) +* [Global Assets Pull Request](https://github.com/abpframework/abp/pull/19968) + diff --git a/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png b/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png new file mode 100644 index 0000000000..8c5c52a4fc Binary files /dev/null and b/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md new file mode 100644 index 0000000000..c07509d2e3 --- /dev/null +++ b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md @@ -0,0 +1,385 @@ +# How to Use OpenAI API with ABP Framework + +In this article, I will show you how to integrate and use the [OpenAI API](https://github.com/openai/openai-dotnet?tab=readme-ov-file#getting-started) with the [ABP Framework](https://abp.io/). We will explore step-by-step how these technologies can work together to enhance your application with powerful AI capabilities, such as natural language processing, image generation, and more. + +![cover-image](cover-image.png) + +## Creating an ABP Project + +To begin integrating OpenAI API with ABP Framework, you first need to create an ABP project. Follow these steps to create and set up your ABP project: +### Step 1: Install ABP CLI + +The ABP CLI is a command-line interface tool that helps you create and manage ABP projects easily. To install the ABP CLI, run the following command in your terminal: + +```bash +dotnet tool install -g Volo.Abp.Studio.Cli +``` + +### Step 2: Create a New ABP Project + +Once you have installed the ABP CLI, you can create a new ABP project using the following command: + +```bash +abp new Acme.OpenAIIntegration -t app --ui-framework mvc --database-provider ef -dbms PostgreSQL --csf +``` + +> This command will generate a complete ABP project with an [MVC UI](https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall). The examples provided in this article make use of UI controllers for demonstration purposes. However, the same approach can easily be applied to other UI types supported by ABP, such as Blazor or Angular. You can find other options [here](https://abp.io/docs/latest/cli). +## OpenAI Integration Setup + +To begin integrating OpenAI API with ABP Framework, follow these steps: + +### Step 1: Create an API Key + +To use the OpenAI services, you first need an API key. To obtain one, first [create a new OpenAI account](https://platform.openai.com/signup) or [log in](https://platform.openai.com/login). Next, navigate to the [API key page](https://platform.openai.com/account/api-keys) and select "Create new secret key", optionally naming the key. Make sure to save your API key somewhere safe and do not share it with anyone. + +This key will be used to authenticate your application when making requests to the OpenAI endpoints. + +### Step 2: Adding *Microsoft.Extensions.AI* Package + +To integrate OpenAI API with ABP, we use [Microsoft.Extensions.AI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI/). This package offers a unified API for integrating AI services, making it easy for developers to work with different AI providers. You can find more details in [this blog post](https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/). + +To begin integrating OpenAI API with ABP Framework, follow these steps: + +1. Add the **Microsoft.Extensions.AI** and **Microsoft.Extensions.AI.OpenAI** (used to interact specifically with OpenAI services. Additionally, this package has alternatives like [Azure OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI/), [Azure AI Inference](https://www.nuget.org/packages/Microsoft.Extensions.AI.AzureAIInference/), and [Ollama](https://www.nuget.org/packages/Microsoft.Extensions.AI.Ollama/), offering flexibility for developers to choose the AI provider that best fits their needs) packages: + +```bash +dotnet add package Microsoft.Extensions.AI --prerelease +dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease +``` + +2. Add the required configuration to the `appsettings.json` file located inside the `Acme.OpenAIIntegration.Web` project and dependencies to your `ConfigureServices` method: + +```json +"AI": { + "OpenAI": { + "Key": "YOUR-API-KEY", + "Chat": { + "ModelId": "gpt-4o-mini" + } + } +} +``` + +> Replace the value of the `Key` with your OpenAI API key. + +Next, add the following code to the `ConfigureServices` method in `OpenAIIntegrationBlazorModule`: + +```csharp +context.Services.AddSingleton(new OpenAIClient(configuration["AI:OpenAI:Key"])); + +context.Services.AddChatClient(services => + services.GetRequiredService().AsChatClient(configuration["AI:OpenAI:Chat:ModelId"] ?? "gpt-4o-mini")); +``` + +## Creating a Sample Page + +To demonstrate the use of OpenAI API, let's create a page named `Sample` in the `Acme.OpenAIIntegration.Web` project: + +Create a `Sample` folder under the `Pages` folder of the `Acme.OpenAIIntegration.Web` project. Add a new Razor Page by right-clicking the `Sample` folder then selecting `Add > Razor Page`. Name it `Index`. + +Open the `Index.cshtml` and change the whole content as shown below: + +> Note: This example demonstrates a simple implementation of a sample page that interacts with the OpenAI API, covering chat, [retrieval-augmented generation (RAG)](https://github.com/openai/openai-dotnet?tab=readme-ov-file#how-to-use-assistants-with-retrieval-augmented-generation-rag), and image generation features. Each example is explained in detail in the next section, so feel free to continue for a better understanding of the steps and logic involved. + +```html +@page +@model Acme.OpenAIIntegration.Web.Pages.Sample +@{ + ViewData["Title"] = "OpenAI API Demonstration"; +} + +

    @ViewData["Title"]

    + +

    + +
    +
    +

    Chat Example

    +
    +
    + + +
    + +
    + @if (!string.IsNullOrEmpty(Model.ChatResponse)) + { +

    Response:

    +

    @Model.ChatResponse

    + } +
    + +
    +

    RAG Example

    +
    +
    + + +
    + +
    + @if (!string.IsNullOrEmpty(Model.RAGResponse)) + { +

    Result:

    +

    @Model.RAGResponse

    + } +
    + +
    +

    Image Generation Example

    +
    +
    + + +
    + +
    + @if (Model.GeneratedImageBytes != null) + { +

    Generated Image:

    + Generated image + } +
    +
    +``` + +`Index.cshtml.cs` content should be like that: + +```csharp +using System; +using System.ClientModel; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.AI; +using OpenAI; +using OpenAI.Assistants; +using OpenAI.Files; +using OpenAI.Images; + +namespace Acme.OpenAIIntegration.Web.Pages; + +public class Sample : PageModel +{ + [BindProperty] + public string ChatInput { get; set; } + public string ChatResponse { get; set; } + + [BindProperty] + public string RAGQuery { get; set; } + public string RAGResponse { get; set; } + + [BindProperty] + public string ImagePrompt { get; set; } + public byte[] GeneratedImageBytes { get; set; } + + private readonly IChatClient _chatClient; + private readonly OpenAIClient _openAiClient; + + public Sample( + IChatClient chatClient, + OpenAIClient openAiClient) + { + _chatClient = chatClient; + _openAiClient = openAiClient; + } + + public async Task OnPostChatAsync() + { + ChatResponse = $"Chat response: {(await _chatClient.CompleteAsync(ChatInput)).Message}"; + return Page(); + } + + public async Task OnPostRAGAsync() + { +#pragma warning disable OPENAI001 + var fileClient = _openAiClient.GetOpenAIFileClient(); + var assistantClient = _openAiClient.GetAssistantClient(); + + using var document = BinaryData.FromBytes(GetExceptionHandlingDocumentContent().ToArray()).ToStream(); + var exceptionHandlingDoc = await fileClient.UploadFileAsync( + document, + "ExceptionHandling.md", + FileUploadPurpose.Assistants); + + AssistantCreationOptions assistantOptions = new() + { + Name = "Exception Handling Assistant", + Instructions = + """ + This assistant helps you with exception handling in ABP Framework. You can ask questions about exception handling and get answers. + + - Do not make any assumptions when asked for information that is not in the document + - Give the most accurate information possible + - Give short(max 1-2 sentence) and concise answers + - Do not provide file citations + """, + + Tools = + { + new FileSearchToolDefinition(), + }, + ToolResources = new() + { + FileSearch = new() + { + NewVectorStores = + { + new VectorStoreCreationHelper([exceptionHandlingDoc.Value.Id]), + } + } + }, + }; + + var assistant = await assistantClient.CreateAssistantAsync("gpt-4o", assistantOptions); + + ThreadCreationOptions threadOptions = new() + { + InitialMessages = { RAGQuery } + }; + + ThreadRun threadRun = assistantClient.CreateThreadAndRun(assistant.Value.Id, threadOptions); + + do + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + threadRun = assistantClient.GetRun(threadRun.ThreadId, threadRun.Id); + } while (!threadRun.Status.IsTerminal); + + CollectionResult messages + = assistantClient.GetMessages(threadRun.ThreadId, + new MessageCollectionOptions() { Order = MessageCollectionOrder.Ascending }); + + var response = new StringBuilder(); + + foreach (var message in messages) + { + response.AppendLine($"[{message.Role.ToString().ToUpper()}]: "); + foreach (var contentItem in message.Content) + { + if (!string.IsNullOrEmpty(contentItem.Text)) + { + response.AppendLine(contentItem.Text); + + if (contentItem.TextAnnotations.Count > 0) + { + response.AppendLine(""); + } + } + } + + response.AppendLine(""); +#pragma warning restore OPENAI001 + } + + RAGResponse = response.ToString(); + + return Page(); + } + + public async Task OnPostImageGenerationAsync() + { + var client = _openAiClient.GetImageClient("dall-e-3"); + + var image = await client.GenerateImageAsync(ImagePrompt, new ImageGenerationOptions + { + ResponseFormat = GeneratedImageFormat.Bytes + }); + + var imageBytes = image.Value.ImageBytes; + + using var memoryStream = new MemoryStream(); + await imageBytes.ToStream().CopyToAsync(memoryStream); + GeneratedImageBytes = memoryStream.ToArray(); + + return Page(); + } + + public ReadOnlySpan GetExceptionHandlingDocumentContent() + { + return """ + # Exception Handling + + ABP provides a built-in infrastructure and offers a standard model for handling exceptions. + + * Automatically **handles all exceptions** and sends a standard **formatted error message** to the client for an API/AJAX request. + * Automatically hides **internal infrastructure errors** and returns a standard error message. + * Provides an easy and configurable way to **localize** exception messages. + * Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map custom exceptions. + + ## Automatic Exception Handling + + `AbpExceptionFilter` handles an exception if **any of the following conditions** are met: + + * Exception is thrown by a **controller action** which returns an **object result** (not a view result). + * The request is an AJAX request (`X-Requested-With` HTTP header value is `XMLHttpRequest`). + * Client explicitly accepts the `application/json` content type (via `accept` HTTP header). + + If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client. + + ## Business Exceptions + + Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception. + + `BusinessException` implements the `IBusinessException` interface in addition to the `IHasErrorCode`, `IHasErrorDetails` and `IHasLogLevel` interfaces. The default log level is `Warning`. + + Usually you have an error code related to a particular business exception. For example: + + ````C# + throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer); + ```` + + ### User Friendly Exception + + If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client. + + `UserFriendlyException` class is the built-in implementation of the `IUserFriendlyException` interface. Example usage: + + ````C# + throw new UserFriendlyException( + "Username should be unique!" + ); + ```` + * The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class. + + """u8; + } +} +``` + +## Running the Application + +After completing the setup, you can run the application using the following command: + +```bash + dotnet run --project ./src/Acme.OpenAIIntegration.Web +``` + +Once the application is running, open your browser and navigate to `/Sample`. You should see the `Sample` page we created, which contains sections for Chat, RAG (Retrieval-Augmented Generation), and Image Generation. You can find the screenshot of the page below: + +![sample page](sample-page.png) + +## Examples Overview + +To showcase the integration of the OpenAI API with the ABP Framework, we implemented three different examples: + +1. **Chat Example**: This example demonstrates how to use OpenAI's chat capabilities by allowing users to enter a message and receive an AI-generated response. The implementation involves setting up a simple form on the `Sample` page where users can input their message. The form submission triggers the `OnPostChatAsync` method, which uses the `IChatClient` to generate a response. + +![chat-example](chat-example.gif) + +2. **Retrieval-Augmented Generation (RAG) Example**: In this example, we use OpenAI to answer user queries by referencing custom documents uploaded to the OpenAI API. The implementation involves uploading a document using the `OpenAIFileClient` and creating an assistant with specific instructions to handle the uploaded content. In this case, the document is a section from ABP's Exception Handling documentation, which includes examples on how ABP handles exceptions, user-friendly error messages, and business exceptions. Users can input their query on the `Sample` page, and the `OnPostRAGAsync` method processes the query to generate precise answers based on the document content. If users ask questions that are not covered in the document, the assistant clearly indicates that the information is not available, as per the instructions provided. For example, when asked about `Object Extensions`, the response begins with: "The uploaded document does not contain information about `Object Extensions`...". This demonstrates how the assistant adheres to the provided instructions. You can also find this example illustrated in the GIF below. + +![rag-example-1](rag-example-1.gif) + +![rag-example-2](rag-example-2.gif) + +3. **Image Generation Example**: This example leverages the [DALL-E](https://openai.com/index/dall-e-3/) model to generate images based on user-provided prompts. On the `Sample` page, users can provide a description of the image they want to generate, and the `OnPostImageGenerationAsync` method uses the `OpenAIClient` to generate the image. + +![image-generation-example](image-generation-example.gif) + +## Conclusion + +In this article, we covered how to integrate the OpenAI API with the ABP Framework by creating a sample project, setting up the OpenAI services, and implementing examples for conversational AI, knowledge-based assistance, and image generation. By following these steps, you can add powerful AI-driven capabilities to your application, making it more interactive, intelligent, and capable of meeting user needs effectively. \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/chat-example.gif b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/chat-example.gif new file mode 100644 index 0000000000..f67d54cecc Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/chat-example.gif differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/cover-image.png b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/cover-image.png new file mode 100644 index 0000000000..b1cc16f0b5 Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/cover-image.png differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/image-generation-example.gif b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/image-generation-example.gif new file mode 100644 index 0000000000..c415f95119 Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/image-generation-example.gif differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-1.gif b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-1.gif new file mode 100644 index 0000000000..ca42bf3f61 Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-1.gif differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-2.gif b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-2.gif new file mode 100644 index 0000000000..5f03e453c6 Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-2.gif differ diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/sample-page.png b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/sample-page.png new file mode 100644 index 0000000000..211f63215d Binary files /dev/null and b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/sample-page.png differ diff --git a/docs/en/Community-Articles/2024-12-09-Unit-Test/POST.md b/docs/en/Community-Articles/2024-12-09-Unit-Test/POST.md new file mode 100644 index 0000000000..7c1b74f3ea --- /dev/null +++ b/docs/en/Community-Articles/2024-12-09-Unit-Test/POST.md @@ -0,0 +1,234 @@ +# The new Unit Test structure in ABP application + +A typical ABP modular project usually consists of three main projects: `Application`, `Domain`, and `EntityFrameworkCore/MongoDB`. In these projects, we may provide many services that require unit testing. + +Using abstract unit test classes involves first writing tests in the `Application` and `Domain` layers that are independent of the storage technology, ensuring the correctness of core business logic. These abstract tests are then implemented in `EntityFrameworkCore` or `MongoDB`. The benefits of this approach include: + +1. **Reduced Coupling**: Core logic tests do not depend on specific storage technologies, so switching databases does not require rewriting test code. +2. **Better Isolation**: Focuses on verifying business logic correctness, avoiding interference from database operations. +3. **Increased Reusability**: The same abstract tests can be reused with different storage implementations. +4. **Easier Maintenance and Extensibility**: Different storage implementations can be extended independently without breaking existing tests. +5. **Faster and More Reliable Tests**: Reduces dependency on databases, making tests faster and more stable. + +## How to migrate old unit tests to the new unit test structure + +Assume our project name is `MyCompanyName.MyProjectName`. + +### Changes to the `MyCompanyName.MyProjectName.Application.Tests` project: + +1. Remove the `MyCompanyName.MyProjectName.Application.Tests` project's `MyProjectNameApplicationCollection` class. +2. Modify the `MyCompanyName.MyProjectName.Application.Tests` project's `MyProjectNameApplicationTestBase` class. + +```csharp +public abstract class MyProjectNameApplicationTestBase : MyProjectNameTestBase + where TStartupModule : IAbpModule +{ + //... +} +``` + +3. Modify the `MyCompanyName.MyProjectName.Application.Tests` project's unit test classes to become abstract unit test classes, such as: `SampleAppServiceTests`. + +```csharp +public abstract class SampleAppServiceTests : MyProjectNameApplicationTestBase + where TStartupModule : IAbpModule +{ + [Fact] + public async Task Initial_Data_Should_Contain_Admin_User() + { + //... + } +} +``` + +### Changes to the `MyCompanyName.MyProjectName.Domain.Tests` project: + +1. Remove the `MyCompanyName.MyProjectName.Domain.Tests` project's `MyProjectNameDomainCollection` class. +2. Modify the `MyCompanyName.MyProjectName.Domain.Tests` project's `MyProjectNameDomainTestBase` class. + +```csharp +public abstract class MyProjectNameDomainTestBase : MyProjectNameTestBase + where TStartupModule : IAbpModule +{ + //... +} +``` + +3. Modify the `MyCompanyName.MyProjectName.Domain.Tests` project's unit test classes to become abstract unit test classes, such as: `SampleDomainTests`. + +```csharp +public abstract class SampleDomainTests : MyProjectNameDomainTestBase + where TStartupModule : IAbpModule +{ + [Fact] + public async Task Should_Set_Email_Of_A_User() + { + //... + } +} +``` + +4. Modify the `MyCompanyName.MyProjectName.Domain.Tests` project's `csproj` and module class. Remove references to `EntityFrameworkCore/MongoDB`. + +`MyCompanyName.MyProjectName.Domain.Tests.csproj`: +```xml + + + //... + + + + + + + +``` + +`MyProjectNameDomainTestModule.cs`: + +```csharp +[DependsOn( + typeof(MyProjectNameDomainModule), + typeof(MyProjectNameTestBaseModule) +)] +public class MyProjectNameDomainTestModule : AbpModule +{ + //... +} +``` + +### Changes to the `MyCompanyName.MyProjectName.EntityFrameworkCore.Tests` project: + +Here, we need to create implementation classes for all abstract unit tests. + +```csharp +[Collection(MyProjectNameTestConsts.CollectionDefinitionName)] +public class EfCoreSampleAppServiceTests : SampleAppServiceTests +{ + //... +} +``` + +```csharp +[Collection(MyProjectNameTestConsts.CollectionDefinitionName)] +public class EfCoreSampleDomainTests : SampleDomainTests +{ + //... +} +``` + +We also need to modify the project's dependencies and module class, which should directly or indirectly reference the `Application` and `Domain` test projects. + +`MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj`: + +```xml + + + //... + + + + + + + +``` + +`MyProjectNameEntityFrameworkCoreTestModule.cs`: + +```csharp +[DependsOn( + typeof(MyProjectNameApplicationTestModule), + typeof(MyProjectNameEntityFrameworkCoreModule), + typeof(AbpEntityFrameworkCoreSqliteModule) + )] +public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule +{ + //... +} +``` + +### Changes to the `MyCompanyName.MyProjectName.MongoDB.Tests` project (skip this step if not using MongoDB): + +Like the `EntityFrameworkCore` project, we need to create implementation classes for all abstract unit tests and modify the project's dependencies and module class. + +```csharp +[Collection(MyProjectNameTestConsts.CollectionDefinitionName)] +public class MongoDBSampleAppServiceTests : SampleAppServiceTests +{ + //... +} +``` + +```csharp +[Collection(MyProjectNameTestConsts.CollectionDefinitionName)] +public class MongoDBSampleDomainTests : SampleDomainTests +{ + //... +} +``` + +```xml + + + //... + + + + + + +``` + +```csharp +[DependsOn( + typeof(MyProjectNameApplicationTestModule), + typeof(MyProjectNameMongoDbModule) +)] +public class MyProjectNameMongoDbTestModule : AbpModule +{ + //... +} +``` + +### Changes to the `MyCompanyName.MyProjectName.Web.Tests` project: + +We need to reference the `EntityFrameworkCore/MongoDB` test projects in this test project. + +```xml + + + //... + + + + + + + + +``` + +```csharp +[DependsOn( + typeof(AbpAspNetCoreTestBaseModule), + typeof(MyProjectNameWebModule), + typeof(MyProjectNameApplicationTestModule), + typeof(MyProjectNameEntityFrameworkCoreTestModule) +)] +public class MyProjectNameWebTestModule : AbpModule +{ + //... +} +``` + +We no longer need the `MyProjectNameWebCollection` class in this project. Please delete it and use `[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]` instead. + +## Conclusion + +This is our new unit test structure. Decoupling unit tests from storage technologies ensures the independence of business logic and allows easy switching between storage implementations. Abstract unit test classes improve test reusability, maintainability, and efficiency, reducing refactoring costs and providing flexibility for future tech updates. + +## References + +- [Unit Test](https://abp.io/docs/latest/testing/unit-tests) +- [Abstract all db-related unit tests](https://github.com/abpframework/abp/pull/17880) diff --git a/docs/en/cli/index.md b/docs/en/cli/index.md index e79c36cb07..645c12a715 100644 --- a/docs/en/cli/index.md +++ b/docs/en/cli/index.md @@ -915,7 +915,7 @@ abp logout ### bundle -This command generates script and style references for ABP Blazor WebAssembly and MAUI Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order ```bundle``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain a Blazor or MAUI Blazor project file(*.csproj). +This command generates script and style references for ABP Blazor WebAssembly and MAUI Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order for ```bundle``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain a Blazor or MAUI Blazor project file(*.csproj). Usage: diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 401f6f7aa5..99eb3faf85 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -5,7 +5,8 @@ "items": [ { "text": "Overview", - "path": "get-started" + "path": "get-started", + "isIndex": true }, { "text": "Single Layer Web Application", @@ -51,7 +52,8 @@ "items": [ { "text": "Overview", - "path": "tutorials" + "path": "tutorials", + "isIndex": true }, { "text": "TODO Application", @@ -60,7 +62,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/todo" + "path": "tutorials/todo", + "isIndex": true }, { "text": "Single-Layer Solution", @@ -79,7 +82,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/book-store" + "path": "tutorials/book-store", + "isIndex": true }, { "text": "1: Creating the Server Side", @@ -130,7 +134,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/book-store-with-abp-suite" + "path": "tutorials/book-store-with-abp-suite", + "isIndex": true }, { "text": "1: Creating the Solution", @@ -161,7 +166,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/modular-crm/index.md" + "path": "tutorials/modular-crm/index.md", + "isIndex": true }, { "text": "1: Creating the Initial Solution", @@ -204,7 +210,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/microservice/index.md" + "path": "tutorials/microservice/index.md", + "isIndex": true }, { "text": "1: Creating the initial solution", @@ -266,14 +273,16 @@ "items": [ { "text": "Overview", - "path": "tools.md" + "path": "tools.md", + "isIndex": true }, { "text": "ABP CLI", "items": [ { "text": "Overview", - "path": "cli" + "path": "cli", + "isIndex": true }, { "text": "New Solution Sample Commands", @@ -286,7 +295,8 @@ "items": [ { "text": "Overview", - "path": "studio" + "path": "studio", + "isIndex": true }, { "text": "Installation", @@ -297,7 +307,8 @@ "items": [ { "text": "Overview", - "path": "studio/overview.md" + "path": "studio/overview.md", + "isIndex": true }, { "text": "Solution Explorer", @@ -340,7 +351,8 @@ "items": [ { "text": "Overview", - "path": "suite" + "path": "suite", + "isIndex": true }, { "text": "How to Install", @@ -363,7 +375,8 @@ "items": [ { "text": "Overview", - "path": "suite/generating-crud-page.md" + "path": "suite/generating-crud-page.md", + "isIndex": true }, { "text": "Creating Many-To-Many Relationship", @@ -415,7 +428,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals" + "path": "framework/fundamentals", + "isIndex": true }, { "text": "Application Startup", @@ -426,7 +440,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/authorization.md" + "path": "framework/fundamentals/authorization.md", + "isIndex": true }, { "text": "Dynamic Claims", @@ -439,7 +454,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/caching.md" + "path": "framework/fundamentals/caching.md", + "isIndex": true }, { "text": "Redis Cache", @@ -460,7 +476,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/dependency-injection.md" + "path": "framework/fundamentals/dependency-injection.md", + "isIndex": true }, { "text": "AutoFac Integration", @@ -493,7 +510,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/validation.md" + "path": "framework/fundamentals/validation.md", + "isIndex": true }, { "text": "FluentValidation Integration", @@ -508,7 +526,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure" + "path": "framework/infrastructure", + "isIndex": true }, { "text": "Audit Logging", @@ -519,7 +538,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/background-jobs" + "path": "framework/infrastructure/background-jobs", + "isIndex": true }, { "text": "Hangfire Integration", @@ -540,7 +560,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/background-workers" + "path": "framework/infrastructure/background-workers", + "isIndex": true }, { "text": "Quartz Integration", @@ -557,7 +578,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/blob-storing" + "path": "framework/infrastructure/blob-storing", + "isIndex": true }, { "text": "Storage Providers", @@ -631,7 +653,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/emailing.md" + "path": "framework/infrastructure/emailing.md", + "isIndex": true }, { "text": "MailKit Integration", @@ -648,7 +671,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/event-bus" + "path": "framework/infrastructure/event-bus", + "isIndex": true }, { "text": "Local Event Bus", @@ -659,7 +683,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/event-bus/distributed" + "path": "framework/infrastructure/event-bus/distributed", + "isIndex": true }, { "text": "Azure Service Bus Integration", @@ -726,7 +751,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/text-templating" + "path": "framework/infrastructure/text-templating", + "isIndex": true }, { "text": "Razor Integration", @@ -753,14 +779,88 @@ "items": [ { "text": "Overview", - "path": "framework/architecture" + "path": "framework/architecture", + "isIndex": true + }, + { + "text": "Module Development Best Practices", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices", + "isIndex": true + }, + { + "text": "Module Architecture", + "path": "framework/architecture/best-practices/module-architecture.md" + }, + { + "text": "Domain Layer", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/domain-layer-overview.md", + "isIndex": true + }, + { + "text": "Entities", + "path": "framework/architecture/best-practices/entities.md" + }, + { + "text": "Repositories", + "path": "framework/architecture/best-practices/repositories.md" + }, + { + "text": "Domain Services", + "path": "framework/architecture/best-practices/domain-services.md" + } + ] + }, + { + "text": "Application Layer", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/application-layer-overview.md", + "isIndex": true + }, + { + "text": "Application Services", + "path": "framework/architecture/best-practices/application-services.md" + }, + { + "text": "Data Transfer Objects", + "path": "framework/architecture/best-practices/data-transfer-objects.md" + } + ] + }, + { + "text": "Data Access", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/data-access-overview.md", + "isIndex": true + }, + { + "text": "Entity Framework Core Integration", + "path": "framework/architecture/best-practices/entity-framework-core-integration.md" + }, + { + "text": "MongoDB Integration", + "path": "framework/architecture/best-practices/mongodb-integration.md" + } + ] + } + ] }, { "text": "Modularity", "items": [ { "text": "Overview", - "path": "framework/architecture/modularity/basics.md" + "path": "framework/architecture/modularity/basics.md", + "isIndex": true }, { "text": "Plug-In Modules", @@ -771,7 +871,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/modularity/extending/customizing-application-modules-guide.md" + "path": "framework/architecture/modularity/extending/customizing-application-modules-guide.md", + "isIndex": true }, { "text": "Module Entity Extension System", @@ -794,14 +895,16 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design" + "path": "framework/architecture/domain-driven-design", + "isIndex": true }, { "text": "Domain Layer", "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design/domain-layer.md" + "path": "framework/architecture/domain-driven-design/domain-layer.md", + "isIndex": true }, { "text": "Entities & Aggregate Roots", @@ -830,7 +933,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design/application-layer.md" + "path": "framework/architecture/domain-driven-design/application-layer.md", + "isIndex": true }, { "text": "Application Services", @@ -935,14 +1039,16 @@ "items": [ { "text": "Overview", - "path": "framework/api-development" + "path": "framework/api-development", + "isIndex": true }, { "text": "ABP Endpoints", "items": [ { "text": "Overview", - "path": "framework/api-development/standard-apis" + "path": "framework/api-development/standard-apis", + "isIndex": true }, { "text": "Application Configuration", @@ -985,14 +1091,16 @@ "items": [ { "text": "Overview", - "path": "framework/ui" + "path": "framework/ui", + "isIndex": true }, { "text": "MVC / Razor Pages", "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/overall.md" + "path": "framework/ui/mvc-razor-pages/overall.md", + "isIndex": true }, { "text": "Navigation / Menus", @@ -1039,7 +1147,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/tag-helpers" + "path": "framework/ui/mvc-razor-pages/tag-helpers", + "isIndex": true }, { "text": "Components", @@ -1173,7 +1282,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/theming.md" + "path": "framework/ui/mvc-razor-pages/theming.md", + "isIndex": true }, { "text": "The Basic Theme", @@ -1190,7 +1300,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/javascript-api" + "path": "framework/ui/mvc-razor-pages/javascript-api", + "isIndex": true }, { "text": "Localization", @@ -1251,7 +1362,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/customization-user-interface.md" + "path": "framework/ui/mvc-razor-pages/customization-user-interface.md", + "isIndex": true }, { "text": "Entity Action Extensions", @@ -1283,7 +1395,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/blazor/overall.md" + "path": "framework/ui/blazor/overall.md", + "isIndex": true }, { "text": "Navigation / Menu", @@ -1302,7 +1415,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/blazor/theming.md" + "path": "framework/ui/blazor/theming.md", + "isIndex": true }, { "text": "The Basic Theme", @@ -1424,7 +1538,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/overview.md" + "path": "framework/ui/angular/overview.md", + "isIndex": true }, { "text": "Quick Start", @@ -1618,7 +1733,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/theming.md" + "path": "framework/ui/angular/theming.md", + "isIndex": true }, { "text": "Configuration", @@ -1655,7 +1771,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/extensions-overall.md" + "path": "framework/ui/angular/extensions-overall.md", + "isIndex": true }, { "text": "Entity Action Extensions", @@ -1717,7 +1834,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/react-native" + "path": "framework/ui/react-native", + "isIndex": true } ] }, @@ -1726,7 +1844,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/maui" + "path": "framework/ui/maui", + "isIndex": true } ] }, @@ -1755,14 +1874,16 @@ "items": [ { "text": "Overview", - "path": "framework/data" + "path": "framework/data", + "isIndex": true }, { "text": "Entity Framework Core", "items": [ { "text": "Overview", - "path": "framework/data/entity-framework-core" + "path": "framework/data/entity-framework-core", + "isIndex": true }, { "text": "Database Migrations", @@ -1832,7 +1953,8 @@ "items": [ { "text": "Overview", - "path": "solution-templates" + "path": "solution-templates", + "isIndex": true }, { "text": "Template Guide", @@ -2148,7 +2270,8 @@ "items": [ { "text": "Overview", - "path": "modules" + "path": "modules", + "isIndex": true }, { "text": "Account", @@ -2159,7 +2282,8 @@ "items": [ { "text": "Overview", - "path": "modules/account-pro.md" + "path": "modules/account-pro.md", + "isIndex": true }, { "text": "Tenant impersonation & User impersonation", @@ -2226,7 +2350,8 @@ "items": [ { "text": "Overview", - "path": "modules/identity-server.md" + "path": "modules/identity-server.md", + "isIndex": true }, { "text": "IdentityServer Migration Guide", @@ -2249,7 +2374,8 @@ "items": [ { "text": "Overview", - "path": "modules/openiddict.md" + "path": "modules/openiddict.md", + "isIndex": true }, { "text": "OpenIddict Migration Guide", @@ -2309,7 +2435,8 @@ "items": [ { "text": "Overview", - "path": "ui-themes" + "path": "ui-themes", + "isIndex": true }, { "text": "The Basic Theme", @@ -2326,7 +2453,8 @@ "items": [ { "text": "Overview", - "path": "testing/overall.md" + "path": "testing/overall.md", + "isIndex": true }, { "text": "Unit tests", @@ -2347,7 +2475,8 @@ "items": [ { "text": "Overview", - "path": "deployment" + "path": "deployment", + "isIndex": true }, { "text": "Configuring SSL certificate(HTTPS)", @@ -2380,7 +2509,8 @@ "items": [ { "text": "Overview", - "path": "samples" + "path": "samples", + "isIndex": true }, { "text": "EventHub", @@ -2405,7 +2535,8 @@ "items": [ { "text": "Overview", - "path": "https://abp.io/books" + "path": "https://abp.io/books", + "isIndex": true }, { "text": "Mastering ABP Framework", @@ -2422,7 +2553,8 @@ "items": [ { "text": "Overview", - "path": "release-info" + "path": "release-info", + "isIndex": true }, { "text": "Release Notes", diff --git a/docs/en/framework/architecture/domain-driven-design/specifications.md b/docs/en/framework/architecture/domain-driven-design/specifications.md index 867080b86e..68ff038b7e 100644 --- a/docs/en/framework/architecture/domain-driven-design/specifications.md +++ b/docs/en/framework/architecture/domain-driven-design/specifications.md @@ -81,7 +81,7 @@ namespace MyProject { public class CustomerService : ITransientDependency { - public async Task BuyAlcohol(Customer customer) + public async Task BookRoom(Customer customer) { if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) { @@ -120,7 +120,7 @@ namespace MyProject _customerRepository = customerRepository; } - public async Task> GetCustomersCanBuyAlcohol() + public async Task> GetCustomersCanBookRoom() { var queryable = await _customerRepository.GetQueryableAsync(); var query = queryable.Where( @@ -254,4 +254,4 @@ Some benefits of using specifications: ### When To Not Use? - **Non business expressions**: Do not use specifications for non business-related expressions and operations. -- **Reporting**: If you are just creating a report, do not create specifications, but directly use `IQueryable` & LINQ expressions. You can even use plain SQL, views or another tool for reporting. DDD does not necessarily care about reporting, so the way you query the underlying data store can be important from a performance perspective. \ No newline at end of file +- **Reporting**: If you are just creating a report, do not create specifications, but directly use `IQueryable` & LINQ expressions. You can even use plain SQL, views or another tool for reporting. DDD does not necessarily care about reporting, so the way you query the underlying data store can be important from a performance perspective. diff --git a/docs/en/framework/infrastructure/audit-logging.md b/docs/en/framework/infrastructure/audit-logging.md index f04bfafbe5..3392f5ac35 100644 --- a/docs/en/framework/infrastructure/audit-logging.md +++ b/docs/en/framework/infrastructure/audit-logging.md @@ -106,6 +106,24 @@ Configure(options => `IgnoredUrls` is the only option. It is a list of ignored URLs prefixes. In the preceding example, all URLs starting with `/products` will be ignored for audit logging. +## AbpAspNetCoreAuditingUrlOptions + +`AbpAspNetCoreAuditingUrlOptions` is the [options object](../fundamentals/options.md) to configure audit logging in the ASP.NET Core layer. You can configure it in the `ConfigureServices` method of your [module](../architecture/modularity/basics.md): + +````csharp +Configure(options => +{ + options.IncludeQuery = true; +}); +```` + +Here, a list of the options you can configure: + +* `IncludeSchema` (default: `false`): If you set to true, it will include the schema in the URL. +* `IncludeHost` (default: `false`): If you set to true, it will include the host in the URL. +* `IncludeQuery` (default: `false`): If you set to true, it will include the query string in the URL. + + ## Enabling/Disabling Audit Logging for Services ### Enable/Disable for Controllers & Actions diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md index c4536885c7..35f761ddf1 100644 --- a/docs/en/framework/infrastructure/background-jobs/index.md +++ b/docs/en/framework/infrastructure/background-jobs/index.md @@ -221,6 +221,13 @@ public class MyModule : AbpModule } ```` +* `JobPollPeriod` is used to determine the interval between two job polling operations. Default is 5000 ms (5 seconds). +* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. Default is 1000. +* `DefaultFirstWaitDuration` is used to determine the duration to wait before the first retry. Default is 60 seconds. +* `DefaultTimeout` is used to determine the timeout duration for a job. Default is 172800 seconds (2 days). +* `DefaultWaitFactor` is used to determine the factor to increase the wait duration between retries. Default is 2.0. +* `DistributedLockName` is used to determine the distributed lock name to use. Default is `AbpBackgroundJobWorker`. + ### Data Store The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction to store the jobs. diff --git a/docs/en/framework/ui/blazor/global-scripts-styles.md b/docs/en/framework/ui/blazor/global-scripts-styles.md index d53fc3d2ba..a8819df35a 100644 --- a/docs/en/framework/ui/blazor/global-scripts-styles.md +++ b/docs/en/framework/ui/blazor/global-scripts-styles.md @@ -1,83 +1,72 @@ # Blazor UI: Managing Global Scripts & Styles -Some modules may require additional styles or scripts that need to be referenced in **index.html** file. It's not easy to find and update these types of references in Blazor apps. ABP offers a simple, powerful, and modular way to manage global style and scripts in Blazor apps. +You can add your JavaScript and CSS files from your modules or applications to the Blazor global assets system. All the JavaScript and CSS files will be added to the `global.js` and `global.css` files. You can access these files via the following URL in a Blazor WASM project: -To update script & style references without worrying about dependencies, ordering, etc in a project, you can use the [bundle command](../../../cli#bundle). +- https://localhost/global.js +- https://localhost/global.css -You can also add custom styles and scripts and let ABP manage them for you. In your Blazor project, you can create a class implementing `IBundleContributor` interface. +## Add JavaScript and CSS to the global assets system in the module -`IBundleContributor` interface contains two methods. +Your module project solution will have two related Blazor projects: -* `AddScripts(...)` -* `AddStyles(...)` +* `MyModule.Blazor`:This project includes the JavaScript/CSS files required for your Blazor components. The `MyApp.Blazor.Client (Blazor WASM)` project will reference this project. +* `MyModule.Blazor.WebAssembly.Bundling`:This project is used to add your JavaScript/CSS files to the Blazor global resources. The `MyModule.Blazor (ASP.NET Core)` project will reference this project. -Both methods get `BundleContext` as a parameter. You can add scripts and styles to the `BundleContext` and run [bundle command](../../../cli#bundle). Bundle command detects custom styles and scripts with module dependencies and updates `index.html` file. +You need to define JavaScript and CSS contributor classes in the `MyModule.Blazor.WebAssembly.Bundling` project to add the files to the global assets system. -## Example Usage -```csharp -namespace MyProject.Blazor +> Please use `BlazorWebAssemblyStandardBundles.Scripts.Global` and `BlazorWebAssemblyStandardBundles.Styles.Global` for the bundle name. + +```cs +public class MyModuleBundleScriptContributor : BundleContributor { - public class MyProjectBundleContributor : IBundleContributor + public override void ConfigureBundle(BundleConfigurationContext context) { - public void AddScripts(BundleContext context) - { - context.Add("site.js"); - } - - public void AddStyles(BundleContext context) - { - context.Add("main.css"); - context.Add("custom-styles.css"); - } + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.js"); } } ``` -> There is a BundleContributor class implementing `IBundleContributor` interface coming by default with the startup templates. So, most of the time, you don't need to add it manually. - -## Bundling And Minification -`abp bundle` command offers bundling and minification support for client-side resources(JavaScript and CSS files). `abp bundle` command reads the `appsettings.json` file inside the Blazor project and bundles the resources according to the configuration. You can find the bundle configurations inside `AbpCli.Bundle` element. - -Here are the options that you can control inside the `appsettings.json` file. - -`Mode`: Bundling and minification mode. Possible values are -* `BundleAndMinify`: Bundle all the files into a single file and minify the content. -* `Bundle`: Bundle all files into a single file, but not minify. -* `None`: Add files individually, do not bundle. - -`Name`: Bundle file name. Default value is `global`. - -`Parameters`: You can define additional key/value pair parameters inside this section. `abp bundle` command automatically sends these parameters to the bundle contributors, and you can check these parameters inside the bundle contributor, take some actions according to these values. - -Let's say that you want to exclude some resources from the bundle and control this action using the bundle parameters. You can add a parameter to the bundle section like below. - -```json -"AbpCli": { - "Bundle": { - "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ - "Name": "global", - "Parameters": { - "ExcludeThemeFromBundle":"true" - } - } - } -``` - -You can check this parameter and take action like below. - -```csharp -public class MyProjectNameBundleContributor : IBundleContributor +```cs +public class MyModuleBundleStyleContributor : BundleContributor { - public void AddScripts(BundleContext context) + public override void ConfigureBundle(BundleConfigurationContext context) { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/mystyle.css"); } +} +``` - public void AddStyles(BundleContext context) +```cs +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class MyBlazorWebAssemblyBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) { - var excludeThemeFromBundle = bool.Parse(context.Parameters.GetValueOrDefault("ExcludeThemeFromBundle")); - context.Add("mytheme.css", excludeFromBundle: excludeThemeFromBundle); - context.Add("main.css"); + Configure(options => + { + // Add script bundle + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global) + .AddContributors(typeof(MyModuleBundleScriptContributor)); + + // Add style bundle + options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global) + .AddContributors(typeof(MyModuleBundleStyleContributor)); + }); } } ``` +## Add JavaScript and CSS to the global assets system in the application + +This is similar to the module. You need to define JavaScript and CSS contributor classes in the `MyApp.Blazor.Client` project to add the files to the global assets system. + +## AbpBundlingGlobalAssetsOptions + +You can configure the JavaScript and CSS file names in the `GlobalAssets` property of the `AbpBundlingOptions` class. The default values are `global.js` and `global.css`. + +## Reference + +- [ASP.NET Core MVC Bundling & Minification](../mvc-razor-pages/bundling-minification#bundle-contributorsg) +- [ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app](https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md) diff --git a/docs/en/get-started/index.md b/docs/en/get-started/index.md index 6ffbfe548f..59f9812563 100644 --- a/docs/en/get-started/index.md +++ b/docs/en/get-started/index.md @@ -5,7 +5,7 @@ Great that you've decided to create a new application with ABP. ABP provides mul Please select one of the following documents best fits for your application: - **[Single-Layer Solution](single-layer-web-application.md)**: Creates a single-project solution. Recommended for building an application with a **simpler and easy to understand** architecture. -- **[Layered Solution](layered-web-application.md)**: A fully layered (multiple projects) solution based on [Domain Driven Design](../framework/architecture/domain-driven-design) practices. Recommended for long-term projects that need a **maintainable and extensible** codebase. +- **[Application (Layered)](layered-web-application.md)**: A fully layered (multiple projects) solution based on [Domain Driven Design](../framework/architecture/domain-driven-design) practices. Recommended for long-term projects that need a **maintainable and extensible** codebase. - **[Microservice Solution](microservice.md)**: A **distributed solution** to build **microservice systems**. It includes pre-built services, API gateways, web and mobile applications, Kubernetes and Helm configuration, and everything you need to start your large-scale microservice solution. - **Others** - [Empty ASP.NET Core Application](empty-aspnet-core-application.md) diff --git a/docs/en/images/idle-message.png b/docs/en/images/idle-message.png new file mode 100644 index 0000000000..14ea10c9f8 Binary files /dev/null and b/docs/en/images/idle-message.png differ diff --git a/docs/en/images/idle-setting.png b/docs/en/images/idle-setting.png new file mode 100644 index 0000000000..faecb9174c Binary files /dev/null and b/docs/en/images/idle-setting.png differ diff --git a/docs/en/modules/account-pro.md b/docs/en/modules/account-pro.md index 327a274f84..0dd960e97e 100644 --- a/docs/en/modules/account-pro.md +++ b/docs/en/modules/account-pro.md @@ -358,3 +358,4 @@ This module doesn't define any additional distributed event. See the [standard d * [Impersonation](./account/impersonation.md) * [Linked Accounts](./account/linkedaccounts.md) * [Session Management](./account/session-management.md) +* [Idle Session Timeout](./account/idle-session-timeout.md)] diff --git a/docs/en/modules/account/idle-session-timeout.md b/docs/en/modules/account/idle-session-timeout.md new file mode 100644 index 0000000000..069e6db665 --- /dev/null +++ b/docs/en/modules/account/idle-session-timeout.md @@ -0,0 +1,19 @@ +# Idle Session Timeout + +The `Idle Session Timeout` feature allows you to automatically log out users after a certain period of inactivity. + +## Configure Idle Session Timeout + +You can enable/disable the `Idle Session Timeout` feature in the `Setting > Account > Idle Session Timeout` page. + +The default idle session timeout is 1 hour. You can change it by selecting a different value from the dropdown list or entering a custom value(in minutes). + +![idle-setting](../../images/idle-setting.png) + +Once the idle session timeout is reached, the user will see a warning modal before being logged out. if user does not respond for 60 seconds, the user will be logged out automatically. + +![idle-setting](../../images/idle-message.png) + +## How it works + +There is JavaScript code running in the background to detect user activity. such as mouse movement, key press, click, etc. If there is no activity detected for setting time, The warning modal will be shown to the user. diff --git a/docs/en/modules/docs.md b/docs/en/modules/docs.md index dc04339e78..69de3362a6 100644 --- a/docs/en/modules/docs.md +++ b/docs/en/modules/docs.md @@ -490,9 +490,6 @@ Since not every single document in your projects may not have sections or may no For example [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/blob/master/en/getting-started.md): -``` -..... - ​```json //[doc-params] { @@ -502,9 +499,6 @@ For example [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/bl } ​``` -........ -``` - This section will be automatically deleted during render. And f course, those key values must match with the ones in **Parameter document**. ![Interface](../images/docs-section-ui.png) @@ -513,7 +507,7 @@ Now you can use **Scriban** syntax to create sections in your document. For example: -```` +````text {{ if UI == "NG" }} * `-u` argument specifies the UI framework, `angular` in this case. @@ -672,22 +666,18 @@ The **Docs Module** supports referencing previous and next documents. It's usefu To reference the previous and next documents from a document, you should specify the documentation titles and their paths as follows: -``` - - ````json - //[doc-nav] - { - "Previous": { - "Name": "Overall", - "Path": "testing/overall" - }, - "Next": { - "Name": "Integration tests", - "Path": "testing/integration-tests" - } - } - ```` - +```json +//[doc-nav] +{ + "Previous": { + "Name": "Overall", + "Path": "testing/overall" + }, + "Next": { + "Name": "Integration tests", + "Path": "testing/integration-tests" + } +} ``` After you specify the next & previous documents, they will appear at the end of the current documentation like in the following figure: diff --git a/docs/en/modules/openiddict.md b/docs/en/modules/openiddict.md index 5f421d9001..35c5220096 100644 --- a/docs/en/modules/openiddict.md +++ b/docs/en/modules/openiddict.md @@ -279,7 +279,7 @@ UserInfoController -> connect/userinfo > **Device flow** implementation will be done in the commercial module. -#### AbpOpenIddictAspNetCoreOptions +### AbpOpenIddictAspNetCoreOptions `AbpOpenIddictAspNetCoreOptions` can be configured in the `PreConfigureServices` method of your OpenIddict [module](../framework/architecture/modularity/basics.md). diff --git a/docs/en/release-info/migration-guides/openiddict5-to-6.md b/docs/en/release-info/migration-guides/openiddict5-to-6.md new file mode 100644 index 0000000000..f876f86265 --- /dev/null +++ b/docs/en/release-info/migration-guides/openiddict5-to-6.md @@ -0,0 +1,28 @@ +# OpenIddict 5.x to 6.x Migration Guide + +The 6.0 release of OpenIddict is a major release that introduces breaking changes. + +Check this blog [OpenIddict 6.0 general availability](https://kevinchalet.com/2024/12/17/openiddict-6-0-general-availability/) for the new features introduced in OpenIddict 6.0. and the [Migrate to OpenIddict 6.0](https://documentation.openiddict.com/guides/migration/50-to-60) for more information about the changes. + +In this guide, we will explain the changes you need to make to your ABP application. + +## Constant changes + +The following constants have been renamed: + +| Old Constant Name | New Constant Name | +|---------------------------------------------------------------|-----------------------------------------------------------------| +| `OpenIddictConstants.Permissions.Endpoints.Logout` | `OpenIddictConstants.Permissions.Endpoints.EndSession` | +| `OpenIddictConstants.Permissions.Endpoints.Device` | `OpenIddictConstants.Permissions.Endpoints.DeviceAuthorization` | + + +## IdentityModel packages + +If you have a reference to `IdentityModel` directly, please upgrade the necessary package versions to the latest stable version, which is currently 8.3.0: + +* [System.IdentityModel.Tokens.Jwt](https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/) +* [Microsoft.IdentityModel.Protocols.OpenIdConnect](https://www.nuget.org/packages/Microsoft.IdentityModel.Protocols.OpenIdConnect/) +* [Microsoft.IdentityModel.Tokens](https://www.nuget.org/packages/Microsoft.IdentityModel.Tokens/) +* [Microsoft.IdentityModel.JsonWebTokens](https://www.nuget.org/packages/Microsoft.IdentityModel.JsonWebTokens/) + +That's all, it's a simple migration! If you have advanced usage of OpenIddict, please check the [official migration guide](https://documentation.openiddict.com/guides/migration/50-to-60) for more information. diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index e1461e3d0a..00c5ca07a2 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -273,7 +273,7 @@ Alternatively, you can add `IdentityUser` entity (or any other entity) as a navi ### New book dialog -###### ![New book dialog](../images/suite-ui-new-book.png) +### ![New book dialog](../images/suite-ui-new-book.png) ### Book list page diff --git a/docs/en/tutorials/book-store/part-02.md b/docs/en/tutorials/book-store/part-02.md index d06930bab7..43fcb55d47 100644 --- a/docs/en/tutorials/book-store/part-02.md +++ b/docs/en/tutorials/book-store/part-02.md @@ -580,8 +580,6 @@ Open the `Books.razor` and replace the content as the following: @using Volo.Abp.Application.Dtos @using Acme.BookStore.Books @using Acme.BookStore.Localization -@using Microsoft.Extensions.Localization -@inject IStringLocalizer L @inherits AbpCrudPageBase @@ -628,13 +626,21 @@ Open the `Books.razor` and replace the content as the following: + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} ```` > If you see some syntax errors, you can ignore them if your application is properly built and running. Visual Studio still has some bugs with Blazor. * Inherited from `AbpCrudPageBase` which implements all the CRUD details for us. * `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class. -* Injected `IStringLocalizer` (as `L` object) and used for localization. +* `LocalizationResource` is set to the `BookStoreResource` to localize the texts. While the code above is pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index d59208d858..45444ee76f 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -1351,7 +1351,6 @@ Here's the complete code to create the book management CRUD page, that has been @using Acme.BookStore.Localization @using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Components.Web -@inject IStringLocalizer L @inject AbpBlazorMessageLocalizerHelper LH @inherits AbpCrudPageBase @@ -1396,7 +1395,7 @@ Here's the complete code to create the book management CRUD page, that has been Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType.{context.Type}"] + @L[$"Enum:BookType.{context.Type:D}"] + +@code +{ + public Books() // Constructor + { + LocalizationResource = typeof(BookStoreResource); + } +} ```` {{end}} diff --git a/docs/en/tutorials/book-store/part-05.md b/docs/en/tutorials/book-store/part-05.md index 034d9de5b2..0acc5cac9f 100644 --- a/docs/en/tutorials/book-store/part-05.md +++ b/docs/en/tutorials/book-store/part-05.md @@ -420,6 +420,8 @@ Add the following code block to the end of the `Books.razor` file: { public Books() // Constructor { + LocalizationResource = typeof(BookStoreResource); + CreatePolicyName = BookStorePermissions.Books.Create; UpdatePolicyName = BookStorePermissions.Books.Edit; DeletePolicyName = BookStorePermissions.Books.Delete; @@ -509,14 +511,11 @@ var bookStoreMenu = new ApplicationMenuItem( context.Menu.AddItem(bookStoreMenu); //CHECK the PERMISSION -if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); -} +bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" +).RequirePermissions(BookStorePermissions.Books.Default)); ```` You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return value. The final `ConfigureMainMenuAsync` method should be the following: @@ -545,14 +544,11 @@ private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) context.Menu.AddItem(bookStoreMenu); //CHECK the PERMISSION - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); - } + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + ).RequirePermissions(BookStorePermissions.Books.Default)); } ```` diff --git a/docs/en/tutorials/book-store/part-06.md b/docs/en/tutorials/book-store/part-06.md index 25f6cf6465..d24b68f569 100644 --- a/docs/en/tutorials/book-store/part-06.md +++ b/docs/en/tutorials/book-store/part-06.md @@ -41,7 +41,6 @@ Create an `Authors` folder (namespace) in the `Acme.BookStore.Domain` project an ````csharp using System; -using JetBrains.Annotations; using Volo.Abp; using Volo.Abp.Domain.Entities.Auditing; @@ -115,7 +114,6 @@ Created this class inside the `Acme.BookStore.Domain.Shared` project since we wi ````csharp using System; using System.Threading.Tasks; -using JetBrains.Annotations; using Volo.Abp; using Volo.Abp.Domain.Services; diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index 9a21183235..5bf9f90af1 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -1214,14 +1214,11 @@ You will need to declare a `using Acme.BookStore.Authors;` statement to the begi Open the `BookStoreMenuContributor.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following code to the end of the `ConfigureMainMenuAsync` method: ````csharp -if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) -{ - context.Menu.AddItem(new ApplicationMenuItem( +context.Menu.AddItem(new ApplicationMenuItem( "BooksStore.Authors", l["Menu:Authors"], url: "/authors" - )); -} + ).RequirePermissions(BookStorePermissions.Books.Default)); ```` ### Localizations diff --git a/docs/en/tutorials/book-store/part-10.md b/docs/en/tutorials/book-store/part-10.md index 85f094c97f..d4137e3681 100644 --- a/docs/en/tutorials/book-store/part-10.md +++ b/docs/en/tutorials/book-store/part-10.md @@ -1132,6 +1132,8 @@ The final `@code` block should be the following: public Books() // Constructor { + LocalizationResource = typeof(BookStoreResource); + CreatePolicyName = BookStorePermissions.Books.Create; UpdatePolicyName = BookStorePermissions.Books.Edit; DeletePolicyName = BookStorePermissions.Books.Delete; diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index ca1a85fd8d..c3ca5b63b2 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -467,12 +467,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.RemoteServices.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Abstractions", "src\Volo.Abp.AspNetCore.Abstractions\Volo.Abp.AspNetCore.Abstractions.csproj", "{E1051CD0-9262-4869-832D-B951723F4DDE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj", "{2F9BA650-395C-4BE0-8CCB-9978E753562A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj", "{7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google", "src\Volo.Abp.BlobStoring.Google\Volo.Abp.BlobStoring.Google.csproj", "{DEEB5200-BBF9-464D-9B7E-8FC035A27E94}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google.Tests", "test\Volo.Abp.BlobStoring.Google.Tests\Volo.Abp.BlobStoring.Google.Tests.csproj", "{40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.ExceptionHandling.Tests", "test\Volo.Abp.ExceptionHandling.Tests\Volo.Abp.ExceptionHandling.Tests.csproj", "{E50739A7-5E2F-4EB5-AEA9-554115CB9613}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud", "src\Volo.Abp.Sms.TencentCloud\Volo.Abp.Sms.TencentCloud.csproj", "{BE7109C5-7368-4688-8557-4A15D3F4776A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud.Tests", "test\Volo.Abp.Sms.TencenCloud.Tests\Volo.Abp.Sms.TencentCloud.Tests.csproj", "{C753DDD6-5699-45F8-8669-08CE0BB816DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1399,6 +1406,14 @@ Global {E1051CD0-9262-4869-832D-B951723F4DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.Build.0 = Release|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Release|Any CPU.Build.0 = Release|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Release|Any CPU.Build.0 = Release|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.Build.0 = Debug|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1411,6 +1426,14 @@ Global {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Debug|Any CPU.Build.0 = Debug|Any CPU {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Release|Any CPU.ActiveCfg = Release|Any CPU {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Release|Any CPU.Build.0 = Release|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1646,9 +1669,13 @@ Global {DFAF8763-D1D6-4EB4-B459-20E31007FE2F} = {447C8A77-E5F0-4538-8687-7383196D04EA} {DACD4485-61BE-4DE5-ACAE-4FFABC122500} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E1051CD0-9262-4869-832D-B951723F4DDE} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {2F9BA650-395C-4BE0-8CCB-9978E753562A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {DEEB5200-BBF9-464D-9B7E-8FC035A27E94} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E50739A7-5E2F-4EB5-AEA9-554115CB9613} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {BE7109C5-7368-4688-8557-4A15D3F4776A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C753DDD6-5699-45F8-8669-08CE0BB816DE} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs new file mode 100644 index 0000000000..b0f6bc93f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs @@ -0,0 +1,34 @@ +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsMauiBlazorThemingBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = MauiBlazorStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = MauiBlazorStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(MauiBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(MauiStyleContributor)); + }); + + options + .ScriptBundles + .Add(MauiBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(MauiScriptContributor)); + }); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs new file mode 100644 index 0000000000..645b2118c9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiBlazorStandardBundles +{ + public static class Styles + { + public static string Global = "MauiBlazor.Global"; + } + + public static class Scripts + { + public static string Global = "MauiBlazor.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs new file mode 100644 index 0000000000..34cda82bc8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs new file mode 100644 index 0000000000..e5ae6947de --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/bootstrap/css/bootstrap.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/flag-icon/css/flag-icon.css"); + context.Files.AddIfNotContains("_content/Blazorise/blazorise.css"); + context.Files.AddIfNotContains("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); + context.Files.AddIfNotContains("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj new file mode 100644 index 0000000000..e12285964f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj @@ -0,0 +1,16 @@ + + + + + + + net9.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs index 82a05dadb5..9ba85ebe7e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs @@ -1,9 +1,11 @@ -using Volo.Abp.AspNetCore.Components.Web.Theming; +using Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; +using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming; [DependsOn( + typeof(AbpAspNetCoreComponentsMauiBlazorThemingBundlingModule), typeof(AbpAspNetCoreComponentsWebThemingModule), typeof(AbpAspNetCoreComponentsMauiBlazorModule) )] diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs index 5e03821176..6a218b08f0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs @@ -1,7 +1,9 @@ -using Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class ComponentsComponentsBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj index d3a900f185..80322f16de 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj @@ -10,6 +10,7 @@
    + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs index 501fcc0b9f..59a630175f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs @@ -2,7 +2,6 @@ using System.Net; using System.Net.Http; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -31,7 +30,6 @@ public class AbpAspNetCoreComponentsServerModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - StaticWebAssetsLoader.UseStaticWebAssets(context.Services.GetHostingEnvironment(), context.Services.GetConfiguration()); context.Services.AddHttpClient(nameof(BlazorServerLookupApiRequestService)) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor index 02db9f2e30..8c9d070764 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor @@ -1,8 +1,4 @@ -@using Volo.Abp -@implements IDisposable @inject IComponentBundleManager BundleManager -@inject PersistentComponentState PersistentComponentState - @if (ScriptFiles != null) { foreach (var file in ScriptFiles) @@ -20,37 +16,18 @@ private List? ScriptFiles { get; set; } - private PersistingComponentStateSubscription persistingSubscription; - protected override async Task OnInitializedAsync() { - if (BundleName == null) - { - throw new AbpException("The BundleName parameter of the AbpScripts component can not be null!"); - } - - persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistScriptFiles); + ScriptFiles = new List(); - if (PersistentComponentState.TryTakeFromJson>(nameof(ScriptFiles), out var restoredStyleFiles)) - { - ScriptFiles = restoredStyleFiles; - } - else + if (!BundleName.IsNullOrWhiteSpace()) { ScriptFiles = (await BundleManager.GetScriptBundleFilesAsync(BundleName!)).ToList(); } - if (WebAssemblyScriptFiles != null) + if (OperatingSystem.IsBrowser() && WebAssemblyScriptFiles != null) { - ScriptFiles?.AddRange(WebAssemblyScriptFiles); + ScriptFiles.AddIfNotContains(WebAssemblyScriptFiles); } } - - private Task PersistScriptFiles() - { - PersistentComponentState.PersistAsJson(nameof(ScriptFiles), ScriptFiles); - return Task.CompletedTask; - } - - public void Dispose() => persistingSubscription.Dispose(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor index bae9b382f3..bdcfd26bec 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor @@ -1,8 +1,4 @@ -@using Volo.Abp -@implements IDisposable @inject IComponentBundleManager BundleManager -@inject PersistentComponentState PersistentComponentState - @if (StyleFiles != null) { foreach (var file in StyleFiles) @@ -20,37 +16,18 @@ private List? StyleFiles { get; set; } - private PersistingComponentStateSubscription persistingSubscription; - protected override async Task OnInitializedAsync() { - if (BundleName == null) - { - throw new AbpException("The BundleName parameter of the AbpStyles component can not be null!"); - } - - persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistStyleFiles); + StyleFiles = new List(); - if (PersistentComponentState.TryTakeFromJson>(nameof(StyleFiles), out var restoredStyleFiles)) - { - StyleFiles = restoredStyleFiles; - } - else + if (!BundleName.IsNullOrWhiteSpace()) { StyleFiles = (await BundleManager.GetStyleBundleFilesAsync(BundleName!)).ToList(); } if (OperatingSystem.IsBrowser() && WebAssemblyStyleFiles != null) { - StyleFiles?.AddRange(WebAssemblyStyleFiles); + StyleFiles.AddIfNotContains(WebAssemblyStyleFiles); } } - - private Task PersistStyleFiles() - { - PersistentComponentState.PersistAsJson(nameof(StyleFiles), StyleFiles); - return Task.CompletedTask; - } - - public void Dispose() => persistingSubscription.Dispose(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs new file mode 100644 index 0000000000..27bfd4fa49 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = BlazorWebAssemblyStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = BlazorWebAssemblyStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(BlazorWebAssemblyStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorWebAssemblyStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyScriptContributor)); + }); + + options.MinificationIgnoredFiles.Add("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs new file mode 100644 index 0000000000..d24e5d6976 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs new file mode 100644 index 0000000000..49b6ff8ba1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyStandardBundles +{ + public static class Styles + { + public static string Global = "BlazorWebAssembly.Global"; + } + + public static class Scripts + { + public static string Global = "BlazorWebAssembly.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs new file mode 100644 index 0000000000..b50ba58bd4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/bootstrap/css/bootstrap.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/flag-icon/css/flag-icon.css"); + context.Files.AddIfNotContains("_content/Blazorise/blazorise.css"); + context.Files.AddIfNotContains("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); + context.Files.AddIfNotContains("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj new file mode 100644 index 0000000000..c56c744bae --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj @@ -0,0 +1,16 @@ + + + + + + + net9.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs index 38eff974b8..eb4e6dce44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs @@ -1,9 +1,11 @@ using Volo.Abp.AspNetCore.Components.Web.Theming; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; [DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule), typeof(AbpAspNetCoreComponentsWebThemingModule), typeof(AbpAspNetCoreComponentsWebAssemblyModule) )] diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs index d9b5a90e6a..816dd51889 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs @@ -1,7 +1,9 @@ -using Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class ComponentsComponentsBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj index 902163f391..ebaa3e8d43 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj @@ -10,6 +10,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/DaprAspNetCore/AbpDaprEndpointRouteBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/DaprAspNetCore/AbpDaprEndpointRouteBuilderExtensions.cs deleted file mode 100644 index 24f68ef9ab..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/DaprAspNetCore/AbpDaprEndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,296 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Dapr -{ - /// - /// This class defines configurations for the subscribe endpoint. - /// - public class AbpSubscribeOptions - { - /// - /// Gets or Sets a value which indicates whether to enable or disable processing raw messages. - /// - public bool EnableRawPayload { get; set; } - - /// - /// An optional delegate used to configure the subscriptions. - /// - public Func, Task>? SubscriptionsCallback { get; set; } - } - - /// - /// This class defines subscribe endpoint response - /// - public class AbpSubscription - { - /// - /// Gets or sets the topic name. - /// - public string Topic { get; set; } = default!; - - /// - /// Gets or sets the pubsub name - /// - public string PubsubName { get; set; } = default!; - - /// - /// Gets or sets the route - /// - public string? Route { get; set; } - - /// - /// Gets or sets the routes - /// - public AbpRoutes? Routes { get; set; } - - /// - /// Gets or sets the metadata. - /// - public AbpMetadata? Metadata { get; set; } - - /// - /// Gets or sets the deadletter topic. - /// - public string? DeadLetterTopic { get; set; } - } - - /// - /// This class defines the metadata for subscribe endpoint. - /// - public class AbpMetadata : Dictionary - { - /// - /// Initializes a new instance of the Metadata class. - /// - public AbpMetadata() { } - - /// - /// Initializes a new instance of the Metadata class. - /// - /// - public AbpMetadata(IDictionary dictionary) : base(dictionary) { } - - /// - /// RawPayload key - /// - internal const string RawPayload = "rawPayload"; - } - - /// - /// This class defines the routes for subscribe endpoint. - /// - public class AbpRoutes - { - /// - /// Gets or sets the default route - /// - public string? Default { get; set; } - - /// - /// Gets or sets the routing rules - /// - public List? Rules { get; set; } - } - - /// - /// This class defines the rule for subscribe endpoint. - /// - public class AbpRule - { - /// - /// Gets or sets the CEL expression to match this route. - /// - public string Match { get; set; } = default!; - - /// - /// Gets or sets the path of the route. - /// - public string Path { get; set; } = default!; - } -} - -namespace Microsoft.AspNetCore.Builder -{ - using System.Collections.Generic; - using System.Linq; - using System.Text.Json; - using System.Text.Json.Serialization; - using Dapr; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Routing; - using Microsoft.AspNetCore.Routing.Patterns; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - - /// - /// Contains extension methods for . - /// - public static class AbpDaprEndpointRouteBuilderExtensions - { - /// - /// Maps an endpoint that will respond to requests to /dapr/subscribe from the - /// Dapr runtime. - /// - /// The . - /// The . - public static IEndpointConventionBuilder MapAbpSubscribeHandler(this IEndpointRouteBuilder endpoints) - { - return CreateSubscribeEndPoint(endpoints); - } - - /// - /// Maps an endpoint that will respond to requests to /dapr/subscribe from the - /// Dapr runtime. - /// - /// The . - /// Configuration options - /// The . - /// - public static IEndpointConventionBuilder MapAbpSubscribeHandler(this IEndpointRouteBuilder endpoints, AbpSubscribeOptions options) - { - return CreateSubscribeEndPoint(endpoints, options); - } - - private static IEndpointConventionBuilder CreateSubscribeEndPoint(IEndpointRouteBuilder endpoints, AbpSubscribeOptions? options = null) - { - if (endpoints is null) - { - throw new System.ArgumentNullException(nameof(endpoints)); - } - - return endpoints.MapGet("dapr/subscribe", async context => - { - var logger = context.RequestServices.GetService()?.CreateLogger("DaprTopicSubscription"); - var dataSource = context.RequestServices.GetRequiredService(); - var subscriptions = dataSource.Endpoints - .OfType() - .Where(e => e.Metadata.GetOrderedMetadata().Any(t => t.Name != null)) // only endpoints which have TopicAttribute with not null Name. - .SelectMany(e => - { - var topicMetadata = e.Metadata.GetOrderedMetadata(); - var originalTopicMetadata = e.Metadata.GetOrderedMetadata(); - - var subs = new List<(string PubsubName, string Name, string? DeadLetterTopic, bool? EnableRawPayload, string Match, int Priority, Dictionary OriginalTopicMetadata, string? MetadataSeparator, RoutePattern RoutePattern)>(); - - for (int i = 0; i < topicMetadata.Count(); i++) - { - subs.Add((topicMetadata[i].PubsubName, - topicMetadata[i].Name, - (topicMetadata[i] as IDeadLetterTopicMetadata)?.DeadLetterTopic, - (topicMetadata[i] as IRawTopicMetadata)?.EnableRawPayload, - topicMetadata[i].Match, - topicMetadata[i].Priority, - originalTopicMetadata.Where(m => (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.OwnedMetadatas?.Any(o => o.Equals(m.Id)) == true || string.IsNullOrEmpty(m.Id)) - .GroupBy(c => c.Name) - .ToDictionary(m => m.Key, m => m.Select(c => c.Value).Distinct().ToArray()), - (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.MetadataSeparator, - e.RoutePattern)); - } - - return subs; - }) - .Distinct() - .GroupBy(e => new { e.PubsubName, e.Name }) - .Select(e => e.OrderBy(e => e.Priority)) - .Select(e => - { - var first = e.First(); - var rawPayload = e.Any(e => e.EnableRawPayload.GetValueOrDefault()); - var metadataSeparator = e.FirstOrDefault(e => !string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator?.ToString() ?? ","; - var rules = e.Where(e => !string.IsNullOrEmpty(e.Match)).ToList(); - var defaultRoutes = e.Where(e => string.IsNullOrEmpty(e.Match)).Select(e => RoutePatternToString(e.RoutePattern)).ToList(); - var defaultRoute = defaultRoutes.FirstOrDefault(); - - //multiple identical names. use comma separation. - var metadata = new AbpMetadata(e.SelectMany(c => c.OriginalTopicMetadata).GroupBy(c => c.Key).ToDictionary(c => c.Key, c => string.Join(metadataSeparator, c.SelectMany(c => c.Value).Distinct()))); - if (rawPayload || options?.EnableRawPayload is true) - { - metadata.Add(AbpMetadata.RawPayload, "true"); - } - - if (logger != null) - { - if (defaultRoutes.Count > 1) - { - logger.LogError("A default subscription to topic {name} on pubsub {pubsub} already exists.", first.Name, first.PubsubName); - } - - var duplicatePriorities = rules.GroupBy(e => e.Priority) - .Where(g => g.Count() > 1) - .ToDictionary(x => x.Key, y => y.Count()); - - foreach (var entry in duplicatePriorities) - { - logger.LogError("A subscription to topic {name} on pubsub {pubsub} has duplicate priorities for {priority}: found {count} occurrences.", first.Name, first.PubsubName, entry.Key, entry.Value); - } - } - - var subscription = new AbpSubscription - { - Topic = first.Name, - PubsubName = first.PubsubName, - Metadata = metadata.Count > 0 ? metadata : null, - }; - - if (first.DeadLetterTopic != null) - { - subscription.DeadLetterTopic = first.DeadLetterTopic; - } - - // Use the V2 routing rules structure - if (rules.Count > 0) - { - subscription.Routes = new AbpRoutes - { - Rules = rules.Select(e => new AbpRule - { - Match = e.Match, - Path = RoutePatternToString(e.RoutePattern), - }).ToList(), - Default = defaultRoute, - }; - } - // Use the V1 structure for backward compatibility. - else - { - subscription.Route = defaultRoute; - } - - return subscription; - }) - .OrderBy(e => (e.PubsubName, e.Topic)) - .ToList(); - - await options?.SubscriptionsCallback!(subscriptions)!; - await context.Response.WriteAsync(JsonSerializer.Serialize(subscriptions, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - })); - }); - } - - private static string RoutePatternToString(RoutePattern routePattern) - { - return string.Join("/", routePattern.PathSegments - .Select(segment => string.Concat(segment.Parts.Cast() - .Select(part => part.Content)))); - } - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index a291c4e769..a4a4f97f5d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -1,11 +1,16 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Dapr; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Volo.Abp.DependencyInjection; +using Volo.Abp.Dapr; using Volo.Abp.EventBus; using Volo.Abp.EventBus.Dapr; using Volo.Abp.EventBus.Distributed; @@ -21,50 +26,98 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - var subscribeOptions = context.Services.ExecutePreConfiguredActions(); - - Configure(options => + PostConfigure(options => { options.EndpointConfigureActions.Add(endpointContext => { - var rootServiceProvider = endpointContext.ScopeServiceProvider.GetRequiredService(); - subscribeOptions.SubscriptionsCallback = subscriptions => - { - var daprEventBusOptions = rootServiceProvider.GetRequiredService>().Value; - foreach (var handler in rootServiceProvider.GetRequiredService>().Value.Handlers) + var topicMetadatas = endpointContext.Endpoints.DataSources.SelectMany(x => x.Endpoints).OfType() + .Where(e => e.Metadata.GetOrderedMetadata().Any(t => t.Name != null)) + .SelectMany(e => e.Metadata.GetOrderedMetadata()) + .ToList(); + + var endpointConventionBuilder = endpointContext.Endpoints.MapPost( + "/api/abp/dapr/event", async httpContext => { - foreach (var @interface in handler.GetInterfaces().Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDistributedEventHandler<>))) - { - var eventType = @interface.GetGenericArguments()[0]; - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - - if (subscriptions.Any(x => x.PubsubName == daprEventBusOptions.PubSubName && x.Topic == eventName)) - { - // Controllers with a [Topic] attribute can replace built-in event handlers. - continue; - } - - var subscription = new AbpSubscription - { - PubsubName = daprEventBusOptions.PubSubName, - Topic = eventName, - Route = AbpAspNetCoreMvcDaprPubSubConsts.DaprEventCallbackUrl, - Metadata = new AbpMetadata - { - { - AbpMetadata.RawPayload, "true" - } - } - }; - subscriptions.Add(subscription); - } - } - - return Task.CompletedTask; - }; - - endpointContext.Endpoints.MapAbpSubscribeHandler(subscribeOptions); + await HandleEventAsync(httpContext); + }); + + var abpEvents = GetAbpEvents(endpointContext); + foreach (var @event in abpEvents.Where(x => !topicMetadatas.Any(t => t.PubsubName == x.PubsubName && t.Name == x.Name))) + { + endpointConventionBuilder.WithMetadata(new TopicAttribute( + @event.PubsubName, + @event.Name, + true)); + } + + endpointContext.Endpoints.MapSubscribeHandler(); }); }); } + + private List GetAbpEvents(EndpointRouteBuilderContext endpointContext) + { + var subscriptions = new List(); + var daprEventBusOptions = endpointContext.Endpoints.ServiceProvider.GetRequiredService>().Value; + + foreach (var @interface in endpointContext.Endpoints.ServiceProvider.GetRequiredService>().Value.Handlers + .SelectMany(x => x.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDistributedEventHandler<>)))) + { + var eventType = @interface.GetGenericArguments()[0]; + var eventName = EventNameAttribute.GetNameOrDefault(eventType); + + var subscription = new TopicAttribute(daprEventBusOptions.PubSubName, eventName); + subscriptions.Add(subscription); + } + + return subscriptions; + } + + private async static Task HandleEventAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); + + httpContext.ValidateDaprAppApiToken(); + + var daprSerializer = httpContext.RequestServices.GetRequiredService(); + var body = (await JsonDocument.ParseAsync(httpContext.Request.Body)); + + var pubSubName = body.RootElement.GetProperty("pubsubname").GetString(); + var topic = body.RootElement.GetProperty("topic").GetString(); + var data = body.RootElement.GetProperty("data").GetRawText(); + if (pubSubName.IsNullOrWhiteSpace() || topic.IsNullOrWhiteSpace() || data.IsNullOrWhiteSpace()) + { + logger.LogError("Invalid Dapr event request."); + httpContext.Response.StatusCode = 400; + return; + } + + var distributedEventBus = httpContext.RequestServices.GetRequiredService(); + + if (IsAbpDaprEventData(data)) + { + var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); + var eventData = daprSerializer.Deserialize(daprEventData.JsonData, distributedEventBus.GetEventType(daprEventData.Topic)); + await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(daprEventData.Topic), eventData, daprEventData.MessageId, daprEventData.CorrelationId); + } + else + { + var eventData = daprSerializer.Deserialize(data, distributedEventBus.GetEventType(topic!)); + await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(topic!), eventData); + } + + httpContext.Response.StatusCode = 200; + } + + private static bool IsAbpDaprEventData(string data) + { + var document = JsonDocument.Parse(data); + var objects = document.RootElement.EnumerateObject().ToList(); + return objects.Count == 5 && + objects.Any(x => x.Name.Equals("PubSubName", StringComparison.CurrentCultureIgnoreCase)) && + objects.Any(x => x.Name.Equals("Topic", StringComparison.CurrentCultureIgnoreCase)) && + objects.Any(x => x.Name.Equals("MessageId", StringComparison.CurrentCultureIgnoreCase)) && + objects.Any(x => x.Name.Equals("JsonData", StringComparison.CurrentCultureIgnoreCase)) && + objects.Any(x => x.Name.Equals("CorrelationId", StringComparison.CurrentCultureIgnoreCase)); + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprPubSubConsts.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprPubSubConsts.cs deleted file mode 100644 index 5f224abc7e..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprPubSubConsts.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Volo.Abp.AspNetCore.Mvc.Dapr.EventBus; - -public class AbpAspNetCoreMvcDaprPubSubConsts -{ - public const string DaprEventCallbackUrl = "api/abp/dapr/event"; -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/Controllers/AbpAspNetCoreMvcDaprEventsController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/Controllers/AbpAspNetCoreMvcDaprEventsController.cs deleted file mode 100644 index 946c39f59c..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/Controllers/AbpAspNetCoreMvcDaprEventsController.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Volo.Abp.Dapr; -using Volo.Abp.EventBus.Dapr; - -namespace Volo.Abp.AspNetCore.Mvc.Dapr.EventBus.Controllers; - -[Area("abp")] -[RemoteService(Name = "abp")] -public class AbpAspNetCoreMvcDaprEventsController : AbpController -{ - [HttpPost(AbpAspNetCoreMvcDaprPubSubConsts.DaprEventCallbackUrl)] - public virtual async Task EventAsync() - { - HttpContext.ValidateDaprAppApiToken(); - - var daprSerializer = HttpContext.RequestServices.GetRequiredService(); - var body = (await JsonDocument.ParseAsync(HttpContext.Request.Body)); - - var pubSubName = body.RootElement.GetProperty("pubsubname").GetString(); - var topic = body.RootElement.GetProperty("topic").GetString(); - var data = body.RootElement.GetProperty("data").GetRawText(); - if (pubSubName.IsNullOrWhiteSpace() || topic.IsNullOrWhiteSpace() || data.IsNullOrWhiteSpace()) - { - Logger.LogError("Invalid Dapr event request."); - return BadRequest(); - } - - var distributedEventBus = HttpContext.RequestServices.GetRequiredService(); - - if (IsAbpDaprEventData(data)) - { - var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); - var eventData = daprSerializer.Deserialize(daprEventData.JsonData, distributedEventBus.GetEventType(daprEventData.Topic)); - await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(daprEventData.Topic), eventData, daprEventData.MessageId, daprEventData.CorrelationId); - } - else - { - var eventData = daprSerializer.Deserialize(data, distributedEventBus.GetEventType(topic!)); - await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(topic!), eventData); - } - - return Ok(); - } - - protected virtual bool IsAbpDaprEventData(string data) - { - var document = JsonDocument.Parse(data); - var objects = document.RootElement.EnumerateObject().ToList(); - return objects.Count == 5 && - objects.Any(x => x.Name.Equals("PubSubName", StringComparison.CurrentCultureIgnoreCase)) && - objects.Any(x => x.Name.Equals("Topic", StringComparison.CurrentCultureIgnoreCase)) && - objects.Any(x => x.Name.Equals("MessageId", StringComparison.CurrentCultureIgnoreCase)) && - objects.Any(x => x.Name.Equals("JsonData", StringComparison.CurrentCultureIgnoreCase)) && - objects.Any(x => x.Name.Equals("CorrelationId", StringComparison.CurrentCultureIgnoreCase)); - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs new file mode 100644 index 0000000000..206129bec0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs @@ -0,0 +1,20 @@ +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +public class AbpBundlingGlobalAssetsOptions +{ + public bool Enabled { get; set; } + + public string? GlobalStyleBundleName { get; set; } + + public string? GlobalScriptBundleName { get; set; } + + public string JavaScriptFileName { get; set; } + + public string CssFileName { get; set; } + + public AbpBundlingGlobalAssetsOptions() + { + JavaScriptFileName = "global.js"; + CssFileName = "global.css"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs index 64f63c41ec..7512bb8231 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs @@ -28,6 +28,10 @@ public class AbpBundlingOptions public List PreloadStyles { get; } + public AbpBundlingGlobalAssetsOptions GlobalAssets { get; set; } + + public BundleParameterDictionary Parameters { get; set; } + public AbpBundlingOptions() { StyleBundles = new BundleConfigurationCollection(); @@ -37,5 +41,7 @@ public class AbpBundlingOptions DeferScripts = new List(); PreloadStylesByDefault = false; PreloadStyles = new List(); + GlobalAssets = new AbpBundlingGlobalAssetsOptions(); + Parameters = new BundleParameterDictionary(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs index bc4dc14d11..af098a9b2b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs @@ -16,11 +16,14 @@ public class BundleConfigurationContext : IBundleConfigurationContext public IAbpLazyServiceProvider LazyServiceProvider { get; } - public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider) + public BundleParameterDictionary Parameters { get; set; } + + public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider, BundleParameterDictionary? parameters = null) { Files = new List(); ServiceProvider = serviceProvider; LazyServiceProvider = ServiceProvider.GetRequiredService(); FileProvider = fileProvider; + Parameters = parameters ?? new BundleParameterDictionary(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs new file mode 100644 index 0000000000..df4dc573a4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +public class BundleParameterDictionary : Dictionary +{ + public const string InteractiveAutoPropertyName = "InteractiveAuto"; + + public bool InteractiveAuto + { + get + { + return TryGetValue(InteractiveAutoPropertyName, out var value) && bool.Parse(value); + } + set + { + this[InteractiveAutoPropertyName] = value.ToString(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs index f0f7f94439..2292c93a53 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs @@ -1,8 +1,21 @@ -using Volo.Abp.AspNetCore.Mvc.Libs; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; +using Volo.Abp.AspNetCore.VirtualFileSystem; +using Volo.Abp.Bundling.Styles; +using Volo.Abp.AspNetCore.Mvc.Libs; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.Data; using Volo.Abp.Minify; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -23,4 +36,96 @@ public class AbpAspNetCoreMvcUiBundlingModule : AbpModule }); } } + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var environment = context.GetEnvironmentOrNull(); + if (environment != null) + { + environment.WebRootFileProvider = + new CompositeFileProvider( + context.GetEnvironment().WebRootFileProvider, + context.ServiceProvider.GetRequiredService() + ); + } + + await InitialGlobalAssetsAsync(context); + } + + protected virtual async Task InitialGlobalAssetsAsync(ApplicationInitializationContext context) + { + var bundlingOptions = context.ServiceProvider.GetRequiredService>().Value; + var logger = context.ServiceProvider.GetRequiredService>(); + if (!bundlingOptions.GlobalAssets.Enabled) + { + return; + } + + var bundleManager = context.ServiceProvider.GetRequiredService(); + var webHostEnvironment = context.ServiceProvider.GetRequiredService(); + var dynamicFileProvider = context.ServiceProvider.GetRequiredService(); + if (!bundlingOptions.GlobalAssets.GlobalStyleBundleName.IsNullOrWhiteSpace()) + { + var styleFiles = await bundleManager.GetStyleBundleFilesAsync(bundlingOptions.GlobalAssets.GlobalStyleBundleName); + var styles = string.Empty; + foreach (var file in styleFiles) + { + var fileInfo = webHostEnvironment.WebRootFileProvider?.GetFileInfo(file.FileName); + if (fileInfo == null || !fileInfo.Exists) + { + logger.LogError($"Could not find the file: {file.FileName}"); + continue; + } + + var fileContent = await fileInfo.ReadAsStringAsync(); + if (!bundleManager.IsBundlingEnabled()) + { + fileContent = CssRelativePath.Adjust(fileContent, + file.FileName, + Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")); + + styles += $"/*{file.FileName}*/{Environment.NewLine}{fileContent}{Environment.NewLine}{Environment.NewLine}"; + } + else + { + styles += $"{fileContent}{Environment.NewLine}{Environment.NewLine}"; + } + } + + dynamicFileProvider.AddOrUpdate( + new InMemoryFileInfo("/wwwroot/" + bundlingOptions.GlobalAssets.CssFileName, + Encoding.UTF8.GetBytes(styles), + bundlingOptions.GlobalAssets.CssFileName)); + } + + if (!bundlingOptions.GlobalAssets.GlobalScriptBundleName.IsNullOrWhiteSpace()) + { + var scriptFiles = await bundleManager.GetScriptBundleFilesAsync(bundlingOptions.GlobalAssets.GlobalScriptBundleName); + var scripts = string.Empty; + foreach (var file in scriptFiles) + { + var fileInfo = webHostEnvironment.WebRootFileProvider?.GetFileInfo(file.FileName); + if (fileInfo == null || !fileInfo.Exists) + { + logger.LogError($"Could not find the file: {file.FileName}"); + continue; + } + + var fileContent = await fileInfo.ReadAsStringAsync(); + if (!bundleManager.IsBundlingEnabled()) + { + scripts += $"{fileContent.EnsureEndsWith(';')}{Environment.NewLine}{Environment.NewLine}"; + } + else + { + scripts += $"//{file.FileName}{Environment.NewLine}{fileContent.EnsureEndsWith(';')}{Environment.NewLine}{Environment.NewLine}"; + } + } + + dynamicFileProvider.AddOrUpdate( + new InMemoryFileInfo("/wwwroot/" + bundlingOptions.GlobalAssets.JavaScriptFileName, + Encoding.UTF8.GetBytes(scripts), + bundlingOptions.GlobalAssets.JavaScriptFileName)); + } + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs index 20c33e86e5..9ac43585c0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs @@ -172,7 +172,7 @@ public class BundleManager : IBundleManager, ITransientDependency ); } - protected virtual bool IsBundlingEnabled() + public virtual bool IsBundlingEnabled() { switch (Options.Mode) { @@ -240,7 +240,7 @@ public class BundleManager : IBundleManager, ITransientDependency protected virtual BundleConfigurationContext CreateBundleConfigurationContext() { - return new BundleConfigurationContext(ServiceProvider, HostingEnvironment.WebRootFileProvider); + return new BundleConfigurationContext(ServiceProvider, HostingEnvironment.WebRootFileProvider, Options.Parameters); } protected virtual List GetContributors(BundleConfigurationCollection bundles, string bundleName) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs index 6623a2d667..b20e9af7ee 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs @@ -2,6 +2,8 @@ using System; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.VirtualFileSystem; +using Volo.Abp.Bundling.Styles; using Volo.Abp.Minify.Styles; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs index 62dfc66f93..48919e56da 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/ConventionalRouteBuilder.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; @@ -63,7 +64,7 @@ public class ConventionalRouteBuilder : IConventionalRouteBuilder, ITransientDep //Add secondary Id var secondaryIds = action.Parameters .Where(p => p.ParameterName.EndsWith("Id", StringComparison.Ordinal)).ToList(); - if (secondaryIds.Count == 1) + if (secondaryIds.Count == 1 && !secondaryIds[0].Attributes.Any(x => x is OptionalAttribute)) { url += $"/{{{NormalizeSecondaryIdNameCase(secondaryIds[0], configuration)}}}"; } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs index 35d30e9f64..bd767f93fb 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DeviceDetectorNET; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; @@ -57,7 +58,7 @@ public class DefaultAbpRequestLocalizationOptionsProvider : ? new RequestLocalizationOptions() : new RequestLocalizationOptions { - DefaultRequestCulture = DefaultGetRequestCulture(defaultLanguage, languages), + DefaultRequestCulture = GetDefaultRequestCulture(defaultLanguage, languages), SupportedCultures = languages .Select(l => l.CultureName) .Distinct() @@ -87,15 +88,22 @@ public class DefaultAbpRequestLocalizationOptionsProvider : return _requestLocalizationOptions; } - private static RequestCulture DefaultGetRequestCulture(string? defaultLanguage, IReadOnlyList languages) + private static RequestCulture GetDefaultRequestCulture(string? defaultLanguage, IReadOnlyList languages) { if (defaultLanguage == null) { - var firstLanguage = languages.First(); + var firstLanguage = languages.FirstOrDefault() ?? new LanguageInfo("en", "en"); return new RequestCulture(firstLanguage.CultureName, firstLanguage.UiCultureName); } var (cultureName, uiCultureName) = LocalizationSettingHelper.ParseLanguageSetting(defaultLanguage); + + if (languages.Any() && languages.All(l => l.CultureName != cultureName)) + { + var firstLanguage = languages.First(); + return new RequestCulture(firstLanguage.CultureName, firstLanguage.UiCultureName); + } + return new RequestCulture(cultureName, uiCultureName); } @@ -106,4 +114,4 @@ public class DefaultAbpRequestLocalizationOptionsProvider : _requestLocalizationOptions = null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs index cda6aea27f..64fab10ac6 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.AspNetCore.RequestLocalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -56,6 +57,8 @@ public class AbpAspNetCoreModule : AbpModule AddAspNetServices(context.Services); context.Services.AddObjectAccessor(); context.Services.AddAbpDynamicOptions(); + + StaticWebAssetsLoader.UseStaticWebAssets(context.Services.GetHostingEnvironment(), context.Services.GetConfiguration()); } private static void AddAspNetServices(IServiceCollection services) diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAspNetCoreAuditingUrlOptions.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAspNetCoreAuditingUrlOptions.cs new file mode 100644 index 0000000000..7d371aa331 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAspNetCoreAuditingUrlOptions.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.AspNetCore.Auditing; + +public class AbpAspNetCoreAuditingUrlOptions +{ + public bool IncludeSchema { get; set; } + + public bool IncludeHost { get; set; } + + public bool IncludeQuery { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AspNetCoreAuditLogContributor.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AspNetCoreAuditLogContributor.cs index fc010f9846..becdee806d 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AspNetCoreAuditLogContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AspNetCoreAuditLogContributor.cs @@ -1,9 +1,11 @@ using System; using System.Linq; +using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.WebClientInfo; using Volo.Abp.Auditing; @@ -40,7 +42,7 @@ public class AspNetCoreAuditLogContributor : AuditLogContributor, ITransientDepe if (context.AuditInfo.Url == null) { - context.AuditInfo.Url = BuildUrl(httpContext); + context.AuditInfo.Url = GetUrl(context, httpContext); } var clientInfoProvider = context.ServiceProvider.GetRequiredService(); @@ -88,18 +90,29 @@ public class AspNetCoreAuditLogContributor : AuditLogContributor, ITransientDepe context.AuditInfo.HttpStatusCode = httpContext.Response.StatusCode; } - protected virtual string BuildUrl(HttpContext httpContext) + protected virtual string GetUrl(AuditLogContributionContext context, HttpContext httpContext) { - //TODO: Add options to include/exclude query, schema and host + var options = context.ServiceProvider.GetRequiredService>(); + var stringBuilder = new StringBuilder(); - var uriBuilder = new UriBuilder + if (options.Value.IncludeSchema) { - Scheme = httpContext.Request.Scheme, - Host = httpContext.Request.Host.Host, - Path = httpContext.Request.Path.ToString(), - Query = httpContext.Request.QueryString.ToString() - }; - - return uriBuilder.Uri.AbsolutePath; + stringBuilder.Append(httpContext.Request.Scheme); + stringBuilder.Append("://"); + } + + if (options.Value.IncludeHost) + { + stringBuilder.Append(httpContext.Request.Host.Host); + } + + stringBuilder.Append(httpContext.Request.Path.ToString()); + + if (options.Value.IncludeQuery) + { + stringBuilder.Append(httpContext.Request.QueryString.ToString()); + } + + return stringBuilder.ToString(); } } diff --git a/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs b/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs index c66d74b381..3dfc5218d0 100644 --- a/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs +++ b/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs @@ -108,6 +108,11 @@ public static class AbpAuthorizationServiceExtensions return (await authorizationService.AuthorizeAsync(resource, policyName)).Succeeded; } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy, throwing an if not. + /// + /// The providing authorization. + /// The name of the policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, string policyName) { if (!await authorizationService.IsGrantedAsync(policyName)) @@ -117,6 +122,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific requirement for the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The requirement to evaluate the policy against. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, IAuthorizationRequirement requirement) { if (!await authorizationService.IsGrantedAsync(resource, requirement)) @@ -126,6 +137,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, AuthorizationPolicy policy) { if (!await authorizationService.IsGrantedAsync(resource, policy)) @@ -135,6 +152,11 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy, throwing an if not. + /// + /// The providing authorization. + /// The policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, AuthorizationPolicy policy) { if (!await authorizationService.IsGrantedAsync(policy)) @@ -143,6 +165,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The requirements to evaluate the policy against. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, IEnumerable requirements) { if (!await authorizationService.IsGrantedAsync(resource, requirements)) @@ -152,6 +180,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The name of the policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, string policyName) { if (!await authorizationService.IsGrantedAsync(resource, policyName)) diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs index 6fc0daca56..3bc31e7a39 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs @@ -33,6 +33,12 @@ public class AbpBackgroundJobWorkerOptions /// public double DefaultWaitFactor { get; set; } + /// + /// Distributed lock name for the worker. + /// Default value: "AbpBackgroundJobWorker". + /// + public string DistributedLockName { get; set; } + public AbpBackgroundJobWorkerOptions() { MaxJobFetchCount = 1000; @@ -40,5 +46,6 @@ public class AbpBackgroundJobWorkerOptions DefaultFirstWaitDuration = 60; DefaultTimeout = 172800; DefaultWaitFactor = 2.0; + DistributedLockName = "AbpBackgroundJobWorker"; } } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs index 4aeaf59885..4313350098 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs @@ -13,8 +13,6 @@ namespace Volo.Abp.BackgroundJobs; public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroundJobWorker { - protected const string DistributedLockName = "AbpBackgroundJobWorker"; - protected AbpBackgroundJobOptions JobOptions { get; } protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } @@ -39,7 +37,7 @@ public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroun protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { - await using (var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken)) + await using (var handler = await DistributedLock.TryAcquireAsync(WorkerOptions.DistributedLockName, cancellationToken: StoppingToken)) { if (handler != null) { diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor index 29010dded6..2547a7930c 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor @@ -112,7 +112,7 @@ Sortable="@column.Sortable" Displayable="column.Visible"> - @(GetConvertedFieldValue(context, column)) + @((MarkupString)GetConvertedFieldValue(context, column)) } @@ -140,7 +140,7 @@ { if (column.ValueConverter != null) { - @(GetConvertedFieldValue(context, column)) + @((MarkupString)GetConvertedFieldValue(context, column)) } else { diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/EntityAction.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/EntityAction.razor index 9bd07ce7da..d913647264 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Components/EntityAction.razor +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/EntityAction.razor @@ -17,7 +17,8 @@ Disabled=@Disabled> @if(!string.IsNullOrEmpty(Icon)) { - + var iconClass = Text.IsNullOrEmpty() ? "" : "me-1"; + } @Text diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs deleted file mode 100644 index c1cb76aa22..0000000000 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; - -namespace Volo.Abp.Cli.Bundling.Styles; - -internal static class CssRelativePathAdjuster -{ - private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - public static string Adjust( - string cssFileContents, - string absoluteInputFilePath, - string absoluteOutputPath) - { - var matches = _rxUrl.Matches(cssFileContents); - - if (matches.Count <= 0) - { - return cssFileContents; - } - - var cssDirectoryPath = Path.GetDirectoryName(absoluteInputFilePath); - - foreach (Match match in matches) - { - string quoteDelimiter = match.Groups[1].Value; //url('') vs url("") - string relativePathToCss = match.Groups[2].Value; - - // Ignore root relative references - if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) - continue; - - //prevent query string from causing error - var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); - var pathOnly = pathAndQuery[0]; - var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; - - string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); - string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); - - if (!string.IsNullOrEmpty(queryOnly)) - serverRelativeUrl += "?" + queryOnly; - - string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); - - cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); - } - - return cssFileContents; - } - - private static string GetAbsolutePath(string cssFilePath, string pathOnly) - { - return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); - } - - private static readonly string _protocol = "file:///"; - private static string MakeRelative(string baseFile, string file) - { - if (string.IsNullOrEmpty(file)) - return file; - - Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); - Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); - - if (baseUri.IsAbsoluteUri) - { - return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); - } - else - { - return baseUri.ToString(); - } - } -} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs index 0e46b91060..552c815ced 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using Volo.Abp.Bundling; +using Volo.Abp.Bundling.Styles; using Volo.Abp.DependencyInjection; using Volo.Abp.Minify.Styles; @@ -44,7 +45,7 @@ public class StyleBundler : BundlerBase, IStyleBundler, ITransientDependency protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) { - return CssRelativePathAdjuster.Adjust( + return CssRelativePath.Adjust( fileContent, referencePath, bundleDirectory diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index edb61d6a21..b1605bddd2 100644 --- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj +++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj @@ -33,6 +33,9 @@ + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs similarity index 57% rename from framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs rename to framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs index c41f0ca4a4..4ee53124b0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs @@ -2,18 +2,18 @@ using System.IO; using System.Text.RegularExpressions; -namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles; +namespace Volo.Abp.Bundling.Styles; -internal static class CssRelativePath +public static class CssRelativePath { - private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private readonly static Regex RxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static string Adjust( string cssFileContents, string absoluteInputFilePath, string absoluteOutputPath) { - var matches = _rxUrl.Matches(cssFileContents); + var matches = RxUrl.Matches(cssFileContents); if (matches.Count <= 0) { @@ -24,25 +24,29 @@ internal static class CssRelativePath foreach (Match match in matches) { - string quoteDelimiter = match.Groups[1].Value; //url('') vs url("") - string relativePathToCss = match.Groups[2].Value; + var quoteDelimiter = match.Groups[1].Value; //url('') vs url("") + var relativePathToCss = match.Groups[2].Value; // Ignore root relative references if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) + { continue; + } //prevent query string from causing error var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); var pathOnly = pathAndQuery[0]; var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; - string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); - string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); + var absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); + var serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); if (!string.IsNullOrEmpty(queryOnly)) + { serverRelativeUrl += "?" + queryOnly; + } - string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); + var replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); } @@ -55,22 +59,18 @@ internal static class CssRelativePath return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); } - private static readonly string _protocol = "file:///"; + private const string Protocol = "file:///"; + private static string MakeRelative(string baseFile, string file) { if (string.IsNullOrEmpty(file)) + { return file; + } - Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); - Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); + var baseUri = new Uri(Protocol + baseFile, UriKind.RelativeOrAbsolute); + var fileUri = new Uri(Protocol + file, UriKind.RelativeOrAbsolute); - if (baseUri.IsAbsoluteUri) - { - return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); - } - else - { - return baseUri.ToString(); - } + return baseUri.IsAbsoluteUri ? Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()) : baseUri.ToString(); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/SemaphoreSlimExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/SemaphoreSlimExtensions.cs index 51066da8a5..57151c24a0 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/SemaphoreSlimExtensions.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/SemaphoreSlimExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -6,19 +7,22 @@ namespace Volo.Abp.Threading; public static class SemaphoreSlimExtensions { - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim) { await semaphoreSlim.WaitAsync(); return GetDispose(semaphoreSlim); } - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken) { await semaphoreSlim.WaitAsync(cancellationToken); return GetDispose(semaphoreSlim); } - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout) { if (await semaphoreSlim.WaitAsync(millisecondsTimeout)) { @@ -28,7 +32,8 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout, CancellationToken cancellationToken) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout, CancellationToken cancellationToken) { if (await semaphoreSlim.WaitAsync(millisecondsTimeout, cancellationToken)) { @@ -38,7 +43,8 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim, TimeSpan timeout) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim, TimeSpan timeout) { if (await semaphoreSlim.WaitAsync(timeout)) { @@ -48,7 +54,8 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } - public async static Task LockAsync(this SemaphoreSlim semaphoreSlim, TimeSpan timeout, CancellationToken cancellationToken) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async static ValueTask LockAsync(this SemaphoreSlim semaphoreSlim, TimeSpan timeout, CancellationToken cancellationToken) { if (await semaphoreSlim.WaitAsync(timeout, cancellationToken)) { @@ -58,18 +65,21 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim) { semaphoreSlim.Wait(); return GetDispose(semaphoreSlim); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim, CancellationToken cancellationToken) { semaphoreSlim.Wait(cancellationToken); return GetDispose(semaphoreSlim); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout) { if (semaphoreSlim.Wait(millisecondsTimeout)) @@ -80,6 +90,7 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim, int millisecondsTimeout, CancellationToken cancellationToken) { if (semaphoreSlim.Wait(millisecondsTimeout, cancellationToken)) @@ -90,6 +101,7 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim, TimeSpan timeout) { if (semaphoreSlim.Wait(timeout)) @@ -100,6 +112,7 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IDisposable Lock(this SemaphoreSlim semaphoreSlim, TimeSpan timeout, CancellationToken cancellationToken) { if (semaphoreSlim.Wait(timeout, cancellationToken)) @@ -110,6 +123,7 @@ public static class SemaphoreSlimExtensions throw new TimeoutException(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IDisposable GetDispose(this SemaphoreSlim semaphoreSlim) { return new DisposeAction(static (semaphoreSlim) => diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs index 58d67e1caf..15956b159e 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs @@ -30,12 +30,11 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency Check.NotNullOrWhiteSpace(name, nameof(name)); var key = DistributedLockKeyNormalizer.NormalizeKey(name); - var timeoutReleaser = await _localSyncObjects.LockAsync(key, timeout, cancellationToken); - if (!timeoutReleaser.EnteredSemaphore) + var timeoutReleaser = await _localSyncObjects.LockOrNullAsync(key, timeout, cancellationToken); + if (timeoutReleaser is not null) { - timeoutReleaser.Dispose(); - return null; + return new LocalAbpDistributedLockHandle(timeoutReleaser); } - return new LocalAbpDistributedLockHandle(timeoutReleaser); + return null; } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs index f5953ea3b1..54ddfd0e5e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -36,14 +37,21 @@ public class DbContextEventInbox : IDbContextEventInbox } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = await DbContextProvider.GetDbContextAsync(); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .IncomingEvents .AsNoTracking() .Where(x => !x.Processed) + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) .ToListAsync(cancellationToken: cancellationToken); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs index fecfd1e8ce..a541698b3e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -28,13 +29,20 @@ public class DbContextEventOutbox : IDbContextEventOutbox> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = (IHasEventOutbox)await DbContextProvider.GetDbContextAsync(); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .OutgoingEvents .AsNoTracking() + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) .ToListAsync(cancellationToken: cancellationToken); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs index 6cb37a72d5..be7da15890 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.EntityFrameworkCore.DistributedEvents; public class IncomingEventRecord : BasicAggregateRoot, + IIncomingEventInfo, IHasExtraProperties, IHasCreationTime { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs index 7272c9ac30..625a93b25a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.EntityFrameworkCore.DistributedEvents; public class OutgoingEventRecord : BasicAggregateRoot, + IOutgoingEventInfo, IHasExtraProperties, IHasCreationTime { diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs index 3700f74232..c154330495 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ public interface IEventInbox { Task EnqueueAsync(IncomingEventInfo incomingEvent); - Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default); + Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default); Task MarkAsProcessedAsync(Guid id); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs index 018747945c..bc538ef839 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ public interface IEventOutbox { Task EnqueueAsync(OutgoingEventInfo outgoingEvent); - Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default); + Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default); Task DeleteAsync(Guid id); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs new file mode 100644 index 0000000000..e52325b71f --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs @@ -0,0 +1,17 @@ +using System; +using Volo.Abp.Data; + +namespace Volo.Abp.EventBus.Distributed; + +public interface IIncomingEventInfo : IHasExtraProperties +{ + Guid Id { get; } + + string MessageId { get; } + + string EventName { get; } + + byte[] EventData { get; } + + DateTime CreationTime { get; } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs new file mode 100644 index 0000000000..58dd4a9713 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs @@ -0,0 +1,15 @@ +using System; +using Volo.Abp.Data; + +namespace Volo.Abp.EventBus.Distributed; + +public interface IOutgoingEventInfo : IHasExtraProperties +{ + Guid Id { get; } + + string EventName { get; } + + byte[] EventData { get; } + + DateTime CreationTime { get; } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs new file mode 100644 index 0000000000..935e760546 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.EventBus.Distributed; + +public static class InboxOutboxFilterExpressionTransformer +{ + public static Expression> Transform(Expression> originalExpression) + { + var originalParam = originalExpression.Parameters[0]; + var newParam = Expression.Parameter(typeof(TTarget), originalParam.Name); + var body = ReplaceParameter(originalExpression.Body, originalParam, newParam); + return Expression.Lambda>(body, newParam); + } + + private static Expression ReplaceParameter(Expression body, ParameterExpression oldParam, ParameterExpression newParam) + { + var visitor = new ParameterReplacer(oldParam, newParam); + return visitor.Visit(body); + } + + private class ParameterReplacer : ExpressionVisitor + { + private readonly ParameterExpression _oldParam; + private readonly ParameterExpression _newParam; + + public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam) + { + _oldParam = oldParam; + _newParam = newParam; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + return node == _oldParam ? _newParam : base.VisitParameter(node); + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs index ffd135845a..1be24a3d02 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs @@ -4,7 +4,7 @@ using Volo.Abp.Data; namespace Volo.Abp.EventBus.Distributed; -public class IncomingEventInfo : IHasExtraProperties +public class IncomingEventInfo : IIncomingEventInfo { public static int MaxEventNameLength { get; set; } = 256; diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs index 43e9c42bf8..74b5bca7d4 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs @@ -4,7 +4,7 @@ using Volo.Abp.Data; namespace Volo.Abp.EventBus.Distributed; -public class OutgoingEventInfo : IHasExtraProperties +public class OutgoingEventInfo : IOutgoingEventInfo { public static int MaxEventNameLength { get; set; } = 256; diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs index 5d3db22726..92399b43b0 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs @@ -20,7 +20,7 @@ public class AbpEventBusRabbitMqModule : AbpModule { context .ServiceProvider - .GetRequiredService() + .GetRequiredService() .Initialize(); } } diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs new file mode 100644 index 0000000000..fd88a098a2 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs @@ -0,0 +1,8 @@ +using Volo.Abp.EventBus.Distributed; + +namespace Volo.Abp.EventBus.RabbitMq; + +public interface IRabbitMqDistributedEventBus : IDistributedEventBus +{ + void Initialize(); +} diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 1c8012f529..c84aa6d9e9 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -23,8 +23,8 @@ namespace Volo.Abp.EventBus.RabbitMq; /* TODO: How to handle unsubscribe to unbind on RabbitMq (may not be possible for) */ [Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IDistributedEventBus), typeof(RabbitMqDistributedEventBus))] -public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDependency +[ExposeServices(typeof(IDistributedEventBus), typeof(RabbitMqDistributedEventBus), typeof(IRabbitMqDistributedEventBus))] +public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDistributedEventBus, ISingletonDependency { protected AbpRabbitMqEventBusOptions AbpRabbitMqEventBusOptions { get; } protected IConnectionPool ConnectionPool { get; } @@ -72,7 +72,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe EventTypes = new ConcurrentDictionary(); } - public void Initialize() + public virtual void Initialize() { Consumer = MessageConsumerFactory.Create( new ExchangeDeclareConfiguration( @@ -290,7 +290,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe var eventName = EventNameAttribute.GetNameOrDefault(eventType); var body = Serializer.Serialize(eventData); - return PublishAsync( eventName, body, headersArguments, eventId, correlationId); + return PublishAsync(eventName, body, headersArguments, eventId, correlationId); } protected virtual Task PublishAsync( diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs index 67facf5d42..cc91cad5df 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq.Expressions; namespace Volo.Abp.EventBus.Distributed; @@ -14,11 +15,21 @@ public class AbpEventBusBoxesOptions /// public int InboxWaitingEventMaxCount { get; set; } + /// + /// Default: null, means all events + /// + public Expression>? InboxProcessorFilter { get; set; } + /// /// Default: 1000 /// public int OutboxWaitingEventMaxCount { get; set; } + /// + /// Default: null, means all events + /// + public Expression>? OutboxProcessorFilter { get; set; } + /// /// Period time of and /// Default: 2 seconds @@ -34,7 +45,7 @@ public class AbpEventBusBoxesOptions /// Default: 2 hours /// public TimeSpan WaitTimeToDeleteProcessedInboxEvents { get; set; } - + /// /// Default: true /// @@ -44,7 +55,9 @@ public class AbpEventBusBoxesOptions { CleanOldEventTimeIntervalSpan = TimeSpan.FromHours(6); InboxWaitingEventMaxCount = 1000; + InboxProcessorFilter = null; OutboxWaitingEventMaxCount = 1000; + OutboxProcessorFilter = null; PeriodTimeSpan = TimeSpan.FromSeconds(2); DistributedLockWaitDuration = TimeSpan.FromSeconds(15); WaitTimeToDeleteProcessedInboxEvents = TimeSpan.FromHours(2); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs index e2c0a3c0c6..06014701a2 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -27,7 +28,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency protected DateTime? LastCleanTime { get; set; } - protected string DistributedLockName { get; private set; } = default!; + protected string DistributedLockName { get; set; } = default!; public ILogger Logger { get; set; } protected CancellationTokenSource StoppingTokenSource { get; } protected CancellationToken StoppingToken { get; } @@ -60,7 +61,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency await RunAsync(); } - public Task StartAsync(InboxConfig inboxConfig, CancellationToken cancellationToken = default) + public virtual Task StartAsync(InboxConfig inboxConfig, CancellationToken cancellationToken = default) { InboxConfig = inboxConfig; Inbox = (IEventInbox)ServiceProvider.GetRequiredService(inboxConfig.ImplementationType); @@ -69,7 +70,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken = default) + public virtual Task StopAsync(CancellationToken cancellationToken = default) { StoppingTokenSource.Cancel(); Timer.Stop(cancellationToken); @@ -92,7 +93,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency while (true) { - var waitingEvents = await Inbox.GetWaitingEventsAsync(EventBusBoxesOptions.InboxWaitingEventMaxCount, StoppingToken); + var waitingEvents = await GetWaitingEventsAsync(); if (waitingEvents.Count <= 0) { break; @@ -129,6 +130,11 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency } } + protected virtual async Task> GetWaitingEventsAsync() + { + return await Inbox.GetWaitingEventsAsync(EventBusBoxesOptions.InboxWaitingEventMaxCount, EventBusBoxesOptions.InboxProcessorFilter, StoppingToken); + } + protected virtual async Task DeleteOldEventsAsync() { if (LastCleanTime != null && LastCleanTime + EventBusBoxesOptions.CleanOldEventTimeIntervalSpan > Clock.Now) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index b63133a388..04d07259a3 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -81,88 +81,88 @@ public class LocalDistributedEventBus : IDistributedEventBus, ISingletonDependen return Subscribe(typeof(TEvent), handler); } - public IDisposable Subscribe(Func action) where TEvent : class + public virtual IDisposable Subscribe(Func action) where TEvent : class { return _localEventBus.Subscribe(action); } - public IDisposable Subscribe(ILocalEventHandler handler) where TEvent : class + public virtual IDisposable Subscribe(ILocalEventHandler handler) where TEvent : class { return _localEventBus.Subscribe(handler); } - public IDisposable Subscribe() where TEvent : class where THandler : IEventHandler, new() + public virtual IDisposable Subscribe() where TEvent : class where THandler : IEventHandler, new() { return _localEventBus.Subscribe(); } - public IDisposable Subscribe(Type eventType, IEventHandler handler) + public virtual IDisposable Subscribe(Type eventType, IEventHandler handler) { return _localEventBus.Subscribe(eventType, handler); } - public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class + public virtual IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return _localEventBus.Subscribe(factory); } - public IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) + public virtual IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { return _localEventBus.Subscribe(eventType, factory); } - public void Unsubscribe(Func action) where TEvent : class + public virtual void Unsubscribe(Func action) where TEvent : class { _localEventBus.Unsubscribe(action); } - public void Unsubscribe(ILocalEventHandler handler) where TEvent : class + public virtual void Unsubscribe(ILocalEventHandler handler) where TEvent : class { _localEventBus.Unsubscribe(handler); } - public void Unsubscribe(Type eventType, IEventHandler handler) + public virtual void Unsubscribe(Type eventType, IEventHandler handler) { _localEventBus.Unsubscribe(eventType, handler); } - public void Unsubscribe(IEventHandlerFactory factory) where TEvent : class + public virtual void Unsubscribe(IEventHandlerFactory factory) where TEvent : class { _localEventBus.Unsubscribe(factory); } - public void Unsubscribe(Type eventType, IEventHandlerFactory factory) + public virtual void Unsubscribe(Type eventType, IEventHandlerFactory factory) { _localEventBus.Unsubscribe(eventType, factory); } - public void UnsubscribeAll() where TEvent : class + public virtual void UnsubscribeAll() where TEvent : class { _localEventBus.UnsubscribeAll(); } - public void UnsubscribeAll(Type eventType) + public virtual void UnsubscribeAll(Type eventType) { _localEventBus.UnsubscribeAll(eventType); } - public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) + public virtual Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class { return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); } - public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) + public virtual Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) { return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); } - public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class + public virtual Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class { return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); } - public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + public virtual Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs index 99ee06ca10..2e610d4471 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs @@ -22,7 +22,7 @@ public class OutboxSender : IOutboxSender, ITransientDependency protected IEventOutbox Outbox { get; private set; } = default!; protected OutboxConfig OutboxConfig { get; private set; } = default!; protected AbpEventBusBoxesOptions EventBusBoxesOptions { get; } - protected string DistributedLockName { get; private set; } = default!; + protected string DistributedLockName { get; set; } = default!; public ILogger Logger { get; set; } protected CancellationTokenSource StoppingTokenSource { get; } @@ -77,14 +77,14 @@ public class OutboxSender : IOutboxSender, ITransientDependency { while (true) { - var waitingEvents = await Outbox.GetWaitingEventsAsync(EventBusBoxesOptions.OutboxWaitingEventMaxCount, StoppingToken); + var waitingEvents = await GetWaitingEventsAsync(); if (waitingEvents.Count <= 0) { break; } Logger.LogInformation($"Found {waitingEvents.Count} events in the outbox."); - + if (EventBusBoxesOptions.BatchPublishOutboxEvents) { await PublishOutgoingMessagesInBatchAsync(waitingEvents); @@ -107,6 +107,11 @@ public class OutboxSender : IOutboxSender, ITransientDependency } } + protected virtual async Task> GetWaitingEventsAsync() + { + return await Outbox.GetWaitingEventsAsync(EventBusBoxesOptions.OutboxWaitingEventMaxCount, EventBusBoxesOptions.OutboxProcessorFilter, StoppingToken); + } + protected virtual async Task PublishOutgoingMessagesAsync(List waitingEvents) { foreach (var waitingEvent in waitingEvents) @@ -119,7 +124,7 @@ public class OutboxSender : IOutboxSender, ITransientDependency ); await Outbox.DeleteAsync(waitingEvent.Id); - + Logger.LogInformation($"Sent the event to the message broker with id = {waitingEvent.Id:N}"); } } @@ -129,9 +134,9 @@ public class OutboxSender : IOutboxSender, ITransientDependency await DistributedEventBus .AsSupportsEventBoxes() .PublishManyFromOutboxAsync(waitingEvents, OutboxConfig); - + await Outbox.DeleteManyAsync(waitingEvents.Select(x => x.Id).ToArray()); - + Logger.LogInformation($"Sent {waitingEvents.Count} events to message broker"); } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs index b954578ba6..4bc2a5433c 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs @@ -52,6 +52,11 @@ public static class FeatureCheckerExtensions return false; } + /// + /// Checks if the specified feature is enabled and throws an if it is not. + /// + /// The + /// The name of the feature to be checked. public static async Task CheckEnabledAsync(this IFeatureChecker featureChecker, string featureName) { if (!(await featureChecker.IsEnabledAsync(featureName))) @@ -61,6 +66,13 @@ public static class FeatureCheckerExtensions } } + /// + /// Checks if the specified features are enabled and throws an if they are not. + /// The check can either require all features to be enabled or just one, based on the parameter. + /// + /// The + /// True: Requires all features to be enabled. False: Requires at least one of the features to be enabled. + /// The names of the features to be checked. public static async Task CheckEnabledAsync(this IFeatureChecker featureChecker, bool requiresAll, params string[] featureNames) { if (featureNames.IsNullOrEmpty()) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs index 6743ab5174..bfe52508e4 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using Volo.Abp.Http.Client.ClientProxying; using Volo.Abp.Http.Client.Proxying; namespace Volo.Abp.Http.Client; @@ -8,8 +10,17 @@ public class AbpHttpClientOptions { public Dictionary HttpClientProxies { get; set; } + public Dictionary>> ProxyHttpClientPreSendActions { get; } + public AbpHttpClientOptions() { HttpClientProxies = new Dictionary(); + ProxyHttpClientPreSendActions = new Dictionary>>(); + } + + public AbpHttpClientOptions AddPreSendAction(string remoteServiceName, Action action) + { + ProxyHttpClientPreSendActions.GetOrAdd(remoteServiceName, () => new List>()).Add(action); + return this; } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index e2562d28cd..01c1e2a720 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -146,6 +146,11 @@ public class ClientProxyBase : ITransientDependency HttpResponseMessage response; try { + foreach (var preSendAction in ClientOptions.Value.ProxyHttpClientPreSendActions.Where(x => x.Key == clientConfig.RemoteServiceName).SelectMany(x => x.Value)) + { + preSendAction(clientConfig, requestContext, client); + } + response = await client.SendAsync( requestMessage, HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs index e24dd32199..860a0c9d96 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs @@ -8,17 +8,25 @@ public static class LocalizationSettingHelper /// Gets a setting value like "en-US;en" and returns as splitted values like ("en-US", "en"). /// /// + /// /// - public static (string cultureName, string uiCultureName) ParseLanguageSetting([NotNull] string settingValue) + public static (string cultureName, string uiCultureName) ParseLanguageSetting([NotNull] string settingValue, string defaultCultureName = "en") { Check.NotNull(settingValue, nameof(settingValue)); if (!settingValue.Contains(";")) { - return (settingValue, settingValue); + return CultureHelper.IsValidCultureCode(settingValue) + ? (settingValue, settingValue) + : (defaultCultureName, defaultCultureName); } var splitted = settingValue.Split(';'); - return (splitted[0], splitted[1]); + if (splitted.Length == 2 && CultureHelper.IsValidCultureCode(splitted[0]) && CultureHelper.IsValidCultureCode(splitted[1])) + { + return (splitted[0], splitted[1]); + } + + return (defaultCultureName, defaultCultureName); } } diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs index 55b40fa29b..6a73af27f5 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; @@ -50,16 +51,24 @@ public class MongoDbContextEventInbox : IMongoDbContextEventInb } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = await DbContextProvider.GetDbContextAsync(cancellationToken); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .IncomingEvents .AsQueryable() .Where(x => !x.Processed) + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) + .As>() .ToListAsync(cancellationToken: cancellationToken); return outgoingEventRecords diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs index cc6c4d42df..57b59038b5 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; @@ -40,14 +41,22 @@ public class MongoDbContextEventOutbox : IMongoDbContextEventOu } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = (IHasEventOutbox)await MongoDbContextProvider.GetDbContextAsync(cancellationToken); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .OutgoingEvents.AsQueryable() + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) + .As>() .ToListAsync(cancellationToken: cancellationToken); return outgoingEventRecords diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index e3577bee6e..30733b1e24 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -55,10 +55,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return specificMapper.Map(source); } - var result = TryToMapCollection(scope, source, default); - if (result != null) + if (TryToMapCollection(scope, source, default, out var collectionResult)) { - return result; + return collectionResult; } } @@ -100,10 +99,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return specificMapper.Map(source, destination); } - var result = TryToMapCollection(scope, source, destination); - if (result != null) + if (TryToMapCollection(scope, source, destination, out var collectionResult)) { - return result; + return collectionResult; } } @@ -122,11 +120,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return AutoMap(source, destination); } - protected virtual TDestination? TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination) + protected virtual bool TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult) { if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { - return default; + collectionResult = default!; + return false; } var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); @@ -134,7 +133,8 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency if (specificMapper == null) { //skip, no specific mapper - return default; + collectionResult = default!; + return false; } var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; @@ -209,11 +209,13 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency if (destination != null && destination.GetType().IsArray) { //Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper. - return (TDestination)result; + collectionResult = (TDestination)result; + return true; } //Return the destination if destination exists. The parameter reference equals with return object. - return destination ?? (TDestination)result; + collectionResult = destination ?? (TDestination)result; + return true; } protected virtual bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs index c8a06736b6..e4bbb4fe00 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs @@ -11,10 +11,17 @@ public class AbpSettingOptions public HashSet DeletedSettings { get; } + /// + /// Default: true. + /// This is useful when you change of an existing setting definition to true and don't want to lose the original value. + /// + public bool ReturnOriginalValueIfDecryptFailed { get; set; } + public AbpSettingOptions() { DefinitionProviders = new TypeList(); ValueProviders = new TypeList(); DeletedSettings = new HashSet(); + ReturnOriginalValueIfDecryptFailed = true; } } diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs index 45a3362d30..2af6b7b4af 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Security.Encryption; @@ -10,10 +11,12 @@ public class SettingEncryptionService : ISettingEncryptionService, ITransientDep { protected IStringEncryptionService StringEncryptionService { get; } public ILogger Logger { get; set; } + protected IOptions Options { get; } - public SettingEncryptionService(IStringEncryptionService stringEncryptionService) + public SettingEncryptionService(IStringEncryptionService stringEncryptionService, IOptions options) { StringEncryptionService = stringEncryptionService; + Options = options; Logger = NullLogger.Instance; } @@ -40,7 +43,14 @@ public class SettingEncryptionService : ISettingEncryptionService, ITransientDep } catch (Exception e) { + if (Options.Value.ReturnOriginalValueIfDecryptFailed) + { + Logger.LogWarning(e, "Failed to decrypt the setting: {0}. Returning the original value...", settingDefinition.Name); + return encryptedValue; + } + Logger.LogException(e); + return string.Empty; } } diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml new file mode 100644 index 0000000000..2ad59ce186 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json new file mode 100644 index 0000000000..83fe6d66b9 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json @@ -0,0 +1,63 @@ +{ + "name": "Volo.Abp.Sms.TencentCloud", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.Sms.TencentCloud", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.Sms", + "namespace": "Volo.Abp.Sms", + "name": "AbpSmsModule" + } + ], + "implementingInterfaces": [ + { + "name": "IAbpModule", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IAbpModule" + }, + { + "name": "IOnPreApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" + }, + { + "name": "IOnApplicationInitialization", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationInitialization" + }, + { + "name": "IOnPostApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" + }, + { + "name": "IOnApplicationShutdown", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationShutdown" + }, + { + "name": "IPreConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPreConfigureServices" + }, + { + "name": "IPostConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPostConfigureServices" + } + ], + "contentType": "abpModule", + "name": "AbpSmsTencentCloudModule", + "summary": null + } + ] +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj new file mode 100644 index 0000000000..3077ff392e --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj @@ -0,0 +1,26 @@ + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + enable + Nullable + Volo.Abp.Sms.TencentCloud + Volo.Abp.Sms.TencentCloud + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs new file mode 100644 index 0000000000..b846a53e9d --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Sms.TencentCloud; + +[DependsOn(typeof(AbpSmsModule))] +public class AbpSmsTencentCloudModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("AbpTencentCloudSms")); + } +} diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs new file mode 100644 index 0000000000..cec86d9b36 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.Sms.TencentCloud; + +public class AbpTencentCloudSmsOptions +{ + public string SmsSdkAppId { get; set; } = default!; + + public string SecretKey { get; set; } = default!; + + public string SecretId { get; set; } = default!; + + public string Endpoint { get; set; } = "sms.tencentcloudapi.com"; + + public string Region { get; set; } = "ap-guangzhou"; +} diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs new file mode 100644 index 0000000000..7253a85ba6 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Sms.TencentCloud; + +public static class TencentCloudSmsProperties +{ + public const string SignName = "SignName"; + + public const string TemplateId = "TemplateId"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs new file mode 100644 index 0000000000..fa3c585576 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using TencentCloud.Sms.V20210111; +using TencentCloud.Sms.V20210111.Models; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Sms.TencentCloud; + +public class TencentCloudSmsSender : ISmsSender, ITransientDependency +{ + protected AbpTencentCloudSmsOptions Options { get; } + + public TencentCloudSmsSender(IOptionsMonitor options) + { + Options = options.CurrentValue; + } + + public virtual async Task SendAsync(SmsMessage smsMessage) + { + var client = CreateClient(); + + await client.SendSms(new SendSmsRequest() + { + SmsSdkAppId = Options.SmsSdkAppId, + SignName = smsMessage.Properties.GetOrDefault(TencentCloudSmsProperties.SignName) as string, + TemplateId = smsMessage.Properties.GetOrDefault(TencentCloudSmsProperties.TemplateId) as string, + TemplateParamSet = smsMessage.Text.Split(','), + PhoneNumberSet = [smsMessage.PhoneNumber] + }); + } + + protected virtual SmsClient CreateClient() + { + var credential = new Credential + { + SecretId = Options.SecretId, + SecretKey = Options.SecretKey + }; + var clientProfile = new ClientProfile + { + HttpProfile = new HttpProfile + { + Endpoint = Options.Endpoint + } + }; + return new SmsClient(credential, Options.Region, clientProfile); + } +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index df75ed8799..7870ccff7b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -21,7 +21,7 @@ "Language": "Idioma", "LoadMore": "Cargar más", "ProcessingWithThreeDot": "Procesando...", - "LoadingWithThreeDot": "Cargardo...", + "LoadingWithThreeDot": "Cargando...", "Welcome": "Bienvenido", "Login": "Iniciar sesión", "Register": "Registrarse", diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs index cdaf58b9a4..82a0369aa0 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.AutoMapper.SampleClasses; using Volo.Abp.ObjectMapping; @@ -36,6 +37,13 @@ public class AbpAutoMapperModule_Basic_Tests : AbpIntegratedTest(MyEnum.Value3); + dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3 + } + //[Fact] TODO: Disabled because of https://github.com/AutoMapper/AutoMapper/pull/2379#issuecomment-355899664 /*public void Should_Not_Map_Objects_With_AutoMap_Attributes() { diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..d8b655b4d7 --- /dev/null +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..fb33818660 --- /dev/null +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs index f36322ddaa..295ff6b08e 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs @@ -9,6 +9,8 @@ public class MyMapProfile : Profile { CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap() .MapExtraProperties(ignoredProperties: new[] { "CityName" }); diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs index 5e9107a23b..4dd69041fc 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs @@ -1,8 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Http.Client; using Volo.Abp.Http.Client.ClientProxying; +using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.DynamicProxying; using Volo.Abp.Http.Localization; using Volo.Abp.Localization; @@ -60,5 +63,16 @@ public class AbpHttpClientTestModule : AbpModule options.FormDataConverts.Add(typeof(List), typeof(TestObjectToFormData)); options.PathConverts.Add(typeof(int), typeof(TestObjectToPath)); }); + + Configure(options => + { + options.AddPreSendAction("Default", (_, requestContext, httpclient) => + { + if (requestContext.Action.Name.Equals("TimeOutRequestAsync")) + { + httpclient.Timeout = TimeSpan.FromMilliseconds(1); + } + }); + }); } } diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs index 547f1bf5a7..21786155bf 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs @@ -43,4 +43,7 @@ public interface IRegularTestController Task DeleteByIdAsync(int id); Task AbortRequestAsync(CancellationToken cancellationToken = default); + + Task TimeOutRequestAsync(); + } diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs index b092e67bc8..46b355090b 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs @@ -152,6 +152,14 @@ public class RegularTestController : AbpController, IRegularTestController await Task.Delay(100, cancellationToken); return "AbortRequestAsync"; } + + [HttpGet] + [Route("timeout-request")] + public async Task TimeOutRequestAsync() + { + await Task.Delay(100); + return "TimeOutRequestAsync"; + } } public class Car diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs index f094c756a9..01ec97d734 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -187,4 +188,11 @@ public class RegularTestControllerClientProxy_Tests : AbpHttpClientTestBase var exception = await Assert.ThrowsAsync(async () => await _controller.AbortRequestAsync(cts.Token)); exception.InnerException.InnerException.InnerException.Message.ShouldBe("The client aborted the request."); } + + [Fact] + public async Task TimeOutRequestAsync() + { + var exception = await Assert.ThrowsAsync(async () => await _controller.TimeOutRequestAsync()); + exception.InnerException.InnerException.Message.ShouldBe("The client aborted the request."); + } } diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg new file mode 100644 index 0000000000..a686451fbc --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj new file mode 100644 index 0000000000..cd63f915d1 --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj @@ -0,0 +1,26 @@ + + + + + + net9.0 + + + + + + + + + + + + + + + + Always + + + + diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs new file mode 100644 index 0000000000..dfc666f21f --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.Sms.TencentCloud; + +public class AbpSmsTencentCloudTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs new file mode 100644 index 0000000000..5062c18867 --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Sms.TencentCloud; + +[DependsOn(typeof(AbpSmsTencentCloudModule))] +public class AbpSmsTencentCloudTestsModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("AbpTencentCloudSms")); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs new file mode 100644 index 0000000000..9198f9b86d --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Volo.Abp.Sms.TencentCloud; + +public class TencentCloudSmsSenderTests : AbpSmsTencentCloudTestBase +{ + private readonly ISmsSender _smsSender; + private readonly IConfiguration _configuration; + + public TencentCloudSmsSenderTests() + { + _configuration = GetRequiredService(); + _smsSender = GetRequiredService(); + } + + [Fact] + public async Task SendSms_Test() + { + var config = _configuration.GetSection("AbpTencentCloudSms"); + + // Please fill in the real parameters in the appsettings.json file. + if (config["SecretId"] == "") + { + return; + } + + var msg = new SmsMessage(config["TargetPhoneNumber"], + config["TemplateParam"]); + msg.Properties.Add(TencentCloudSmsProperties.SignName, config["SignName"]); + msg.Properties.Add(TencentCloudSmsProperties.TemplateId, config["TemplateId"]); + + await _smsSender.SendAsync(msg); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json new file mode 100644 index 0000000000..44d4611caf --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json @@ -0,0 +1,13 @@ +{ + "AbpTencentCloudSms": { + "SecretId": "", + "SecretKey": "", + "Region": "", + "SmsSdkAppId": "", + "Endpoint": "", + "TargetPhoneNumber": "", + "SignName": "", + "TemplateId": "", + "TemplateParam": "" + } +} \ No newline at end of file diff --git a/latest-versions.json b/latest-versions.json index e2c60800a3..43d4fbb9ac 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,13 +1,22 @@ [ + { + "version": "9.0.2", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.0.3" + } + }, { "version": "9.0.1", "releaseDate": "", "type": "stable", "message": "", "leptonx": { - "version": "4.0.1" + "version": "4.0.2" } - }, + }, { "version": "9.0.0", "releaseDate": "", @@ -26,4 +35,4 @@ "version": "3.3.1" } } -] \ No newline at end of file +] diff --git a/modules/basic-theme/Volo.Abp.BasicTheme.sln b/modules/basic-theme/Volo.Abp.BasicTheme.sln index 2e65f7a98d..ac0ab4ed3b 100644 --- a/modules/basic-theme/Volo.Abp.BasicTheme.sln +++ b/modules/basic-theme/Volo.Abp.BasicTheme.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.UI. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BasicTheme.Installer", "src\Volo.Abp.BasicTheme.Installer\Volo.Abp.BasicTheme.Installer.csproj", "{3068A87F-3348-4981-8241-2630BC496117}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj", "{D02053D9-10EF-4717-A792-A53F83347816}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {3068A87F-3348-4981-8241-2630BC496117}.Debug|Any CPU.Build.0 = Debug|Any CPU {3068A87F-3348-4981-8241-2630BC496117}.Release|Any CPU.ActiveCfg = Release|Any CPU {3068A87F-3348-4981-8241-2630BC496117}.Release|Any CPU.Build.0 = Release|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {C8068E7F-4A04-4755-8976-C2A4C0ADC708} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} @@ -68,5 +74,6 @@ Global {51B491ED-F959-4974-A876-528B5F16BC92} = {0BC55E3B-4964-48E3-A390-2ADD37980149} {8C336CB8-F7A9-4203-AE55-D8F5FDB2A958} = {0BC55E3B-4964-48E3-A390-2ADD37980149} {3068A87F-3348-4981-8241-2630BC496117} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} + {D02053D9-10EF-4717-A792-A53F83347816} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} EndGlobalSection EndGlobal diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs new file mode 100644 index 0000000000..b6fcb74e34 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs @@ -0,0 +1,20 @@ +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(BasicThemeBundleStyleContributor)); + }); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs new file mode 100644 index 0000000000..de65538929 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; + +public class BasicThemeBundleStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web.BasicTheme/libs/abp/css/theme.css"); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj new file mode 100644 index 0000000000..edf08d8ebe --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj @@ -0,0 +1,15 @@ + + + + + + + net9.0 + Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs index cbc24669d8..08454376c2 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs @@ -3,6 +3,7 @@ using Volo.Abp.AspNetCore.Components.Web; using Volo.Abp.AspNetCore.Components.Web.BasicTheme; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.Web.Theming.Toolbars; +using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; using Volo.Abp.AspNetCore.Components.WebAssembly.Theming; using Volo.Abp.Http.Client.IdentityModel.WebAssembly; using Volo.Abp.Modularity; @@ -10,6 +11,7 @@ using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme; [DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule), typeof(AbpAspNetCoreComponentsWebBasicThemeModule), typeof(AbpAspNetCoreComponentsWebAssemblyThemingModule), typeof(AbpHttpClientIdentityModelWebAssemblyModule) diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs index 67b29987f1..ce07a26c63 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs @@ -1,7 +1,9 @@ -using Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class BasicThemeBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj index d7ee34d13c..c070361f08 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj @@ -12,6 +12,7 @@ +
    diff --git a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs index 6e2383dcbd..47447e1fdf 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs +++ b/modules/blogging/app/Volo.BloggingTestApp/BloggingTestAppModule.cs @@ -1,4 +1,4 @@ -//#define MONGODB +#define MONGODB using System.Collections.Generic; using System.Globalization; @@ -26,9 +26,14 @@ using Volo.Abp.Autofac; using Volo.Abp.BlobStoring; using Volo.Abp.BlobStoring.Database; using Volo.Abp.Data; +#if MONGODB +using Volo.Abp.MongoDB; +#else using Volo.Abp.EntityFrameworkCore; +#endif using Volo.Abp.Identity; using Volo.Abp.Identity.Web; +using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; using Volo.Abp.PermissionManagement.HttpApi; @@ -39,7 +44,11 @@ using Volo.Abp.VirtualFileSystem; using Volo.Blogging; using Volo.Blogging.Admin; using Volo.Blogging.Files; +#if MONGODB +using Volo.BloggingTestApp.MongoDB; +#else using Volo.BloggingTestApp.EntityFrameworkCore; +#endif namespace Volo.BloggingTestApp { @@ -78,7 +87,7 @@ namespace Volo.BloggingTestApp Configure(options => { options.RoutePrefix = null; - options.SingleBlogMode.Enabled = true; + options.SingleBlogMode.Enabled = false; }); Configure(options => @@ -146,6 +155,22 @@ namespace Volo.BloggingTestApp container.UseDatabase(); }); }); + + Configure(options => + { + options.Languages.Add(new LanguageInfo("ar", "ar", "العربية")); + options.Languages.Add(new LanguageInfo("en", "en", "English")); + options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština")); + options.Languages.Add(new LanguageInfo("fi", "fi", "Finnish")); + options.Languages.Add(new LanguageInfo("fr", "fr", "Français")); + options.Languages.Add(new LanguageInfo("sk", "sk", "Slovak")); + options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi")); + options.Languages.Add(new LanguageInfo("it", "it", "Italiano")); + options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe")); + options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português")); + options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文")); + options.Languages.Add(new LanguageInfo("zh-Hant", "zh-Hant", "繁体中文")); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/blogging/app/Volo.BloggingTestApp/Program.cs b/modules/blogging/app/Volo.BloggingTestApp/Program.cs index 2741daf2fe..cb0948096d 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/Program.cs +++ b/modules/blogging/app/Volo.BloggingTestApp/Program.cs @@ -1,6 +1,9 @@ using System; using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; @@ -9,7 +12,7 @@ namespace Volo.BloggingTestApp { public class Program { - public static int Main(string[] args) + public async static Task Main(string[] args) { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() //TODO: Should be configurable! @@ -22,7 +25,14 @@ namespace Volo.BloggingTestApp try { Log.Information("Starting web host."); - CreateHostBuilder(args).Build().Run(); + var builder = WebApplication.CreateBuilder(args); + builder.Host + .UseAutofac() + .UseSerilog(); + await builder.AddApplicationAsync(); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); return 0; } catch (Exception ex) @@ -35,14 +45,5 @@ namespace Volo.BloggingTestApp Log.CloseAndFlush(); } } - - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }) - .UseAutofac() - .UseSerilog(); } } diff --git a/modules/blogging/app/Volo.BloggingTestApp/Startup.cs b/modules/blogging/app/Volo.BloggingTestApp/Startup.cs deleted file mode 100644 index 0f4a310afc..0000000000 --- a/modules/blogging/app/Volo.BloggingTestApp/Startup.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Volo.Abp; -using Volo.Abp.Localization; - -namespace Volo.BloggingTestApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddApplication(); - - services.Configure(options => - { - options.Languages.Add(new LanguageInfo("ar", "ar", "العربية")); - options.Languages.Add(new LanguageInfo("en", "en", "English")); - options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština")); - options.Languages.Add(new LanguageInfo("fi", "fi", "Finnish")); - options.Languages.Add(new LanguageInfo("fr", "fr", "Français")); - options.Languages.Add(new LanguageInfo("sk", "sk", "Slovak")); - options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi")); - options.Languages.Add(new LanguageInfo("it", "it", "Italiano")); - options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe")); - options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português")); - options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "简体中文")); - options.Languages.Add(new LanguageInfo("zh-Hant", "zh-Hant", "繁体中文")); - }); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - app.InitializeApplication(); - } - } -} diff --git a/modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj b/modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj index 4e58d52df0..e157d32f25 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj +++ b/modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml index 926c8073ca..e0264603fb 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Edit.cshtml @@ -2,12 +2,16 @@ @using Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor @using Volo.Blogging.Posts @using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Options +@using Volo.Blogging @using Volo.Blogging.Localization @using Volo.Blogging.Pages.Blogs.Posts @inject IHtmlLocalizer L @model Volo.Blogging.Pages.Blogs.Posts.EditModel +@inject IOptions BloggingUrlOptions @{ ViewBag.PageTitle = "Edit Blog Post"; + var blogShortNameRouteParam = BloggingUrlOptions.Value.SingleBlogMode.Enabled ? null : Model.BlogShortName; } @section styles { @@ -86,7 +90,7 @@ diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogDto.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogDto.cs index 0245b1348e..93c5374281 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogDto.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogDto.cs @@ -11,4 +11,6 @@ public class BlogDto : ExtensibleEntityDto, IHasConcurrencyStamp public string Slug { get; set; } public string ConcurrencyStamp { get; set; } + + public int BlogPostCount { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogAdminAppService.cs index 647c2bde15..e0f2b3311b 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogAdminAppService.cs @@ -1,8 +1,13 @@ using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; namespace Volo.CmsKit.Admin.Blogs; public interface IBlogAdminAppService : ICrudAppService { + Task> GetAllListAsync(); + + Task MoveAllBlogPostsAsync(Guid blogId, Guid? assignToBlogId = null); } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/IMenuItemAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/IMenuItemAdminAppService.cs index a8908129d2..f97fc69ee1 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/IMenuItemAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/IMenuItemAdminAppService.cs @@ -21,4 +21,6 @@ public interface IMenuItemAdminAppService : IApplicationService Task MoveMenuItemAsync(Guid id, MenuItemMoveInput input); Task> GetPageLookupAsync(PageLookupInputDto input); + + Task> GetPermissionLookupAsync(PermissionLookupInputDto inputDto); } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemCreateInput.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemCreateInput.cs index f5ec9c6444..3e2f78aaba 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemCreateInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemCreateInput.cs @@ -27,4 +27,6 @@ public class MenuItemCreateInput : ExtensibleObject public string CssClass { get; set; } public Guid? PageId { get; set; } + + public string RequiredPermissionName { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemUpdateInput.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemUpdateInput.cs index 8767a6192a..7a5ebc76b5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemUpdateInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/MenuItemUpdateInput.cs @@ -24,6 +24,8 @@ public class MenuItemUpdateInput : ExtensibleObject, IHasConcurrencyStamp public string CssClass { get; set; } public Guid? PageId { get; set; } + + public string RequiredPermissionName { get; set; } public string ConcurrencyStamp { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupDto.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupDto.cs new file mode 100644 index 0000000000..37000242e7 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupDto.cs @@ -0,0 +1,8 @@ +namespace Volo.CmsKit.Admin.Menus; + +public class PermissionLookupDto +{ + public string Name { get; set; } + + public string DisplayName { get; set; } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupInputDto.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupInputDto.cs new file mode 100644 index 0000000000..bb4b639da6 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Menus/PermissionLookupInputDto.cs @@ -0,0 +1,6 @@ +namespace Volo.CmsKit.Admin.Menus; + +public class PermissionLookupInputDto +{ + public string Filter { get; set; } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs index d5f32c4bfc..e3ef03e2de 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogAdminAppService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; @@ -20,16 +21,19 @@ namespace Volo.CmsKit.Admin.Blogs; public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppService { protected IBlogRepository BlogRepository { get; } + protected IBlogPostRepository BlogPostRepository { get; } protected BlogManager BlogManager { get; } protected BlogFeatureManager BlogFeatureManager { get; } public BlogAdminAppService( IBlogRepository blogRepository, - BlogManager blogManager, + BlogManager blogManager, + IBlogPostRepository blogPostRepository, BlogFeatureManager blogFeatureManager = null) { BlogRepository = blogRepository; BlogManager = blogManager; + BlogPostRepository = blogPostRepository; BlogFeatureManager = blogFeatureManager; } @@ -37,20 +41,44 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi { var blog = await BlogRepository.GetAsync(id); - return ObjectMapper.Map(blog); + var blogDto = ObjectMapper.Map(blog); + blogDto.BlogPostCount = await BlogPostRepository.GetCountAsync(blogId : blog.Id); + + return blogDto; } public virtual async Task> GetListAsync(BlogGetListInput input) { var totalCount = await BlogRepository.GetCountAsync(input.Filter); - var blogs = await BlogRepository.GetListAsync( + var blogs = await BlogRepository.GetListWithBlogPostCountAsync( input.Filter, input.Sorting, input.MaxResultCount, input.SkipCount); + + var blogDtos = new PagedResultDto(totalCount, ObjectMapper.Map, List>(blogs.Select(x => x.Blog).ToList())); + + foreach (var blogDto in blogDtos.Items) + { + blogDto.BlogPostCount = blogs.First(x => x.Blog.Id == blogDto.Id).BlogPostCount; + } - return new PagedResultDto(totalCount, ObjectMapper.Map, List>(blogs)); + return blogDtos; + } + + public virtual async Task> GetAllListAsync() + { + var blogs = await BlogRepository.GetListWithBlogPostCountAsync(maxResultCount: int.MaxValue); + + var blogDtos = new ListResultDto(ObjectMapper.Map, List>(blogs.Select(x => x.Blog).ToList())); + + foreach (var blogDto in blogDtos.Items) + { + blogDto.BlogPostCount = blogs.First(x => x.Blog.Id == blogDto.Id).BlogPostCount; + } + + return blogDtos; } [Authorize(CmsKitAdminPermissions.Blogs.Create)] @@ -79,10 +107,18 @@ public class BlogAdminAppService : CmsKitAdminAppServiceBase, IBlogAdminAppServi return ObjectMapper.Map(blog); } + + [Authorize(CmsKitAdminPermissions.Blogs.Delete)] + public virtual async Task MoveAllBlogPostsAsync(Guid blogId, Guid? assignToBlogId) + { + var blog = await BlogRepository.GetAsync(blogId); + await BlogPostRepository.UpdateBlogAsync(blog.Id, assignToBlogId); + } [Authorize(CmsKitAdminPermissions.Blogs.Delete)] public virtual Task DeleteAsync(Guid id) { + return BlogRepository.DeleteAsync(id); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs index 1bf9775d30..dd39cddf88 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs @@ -10,6 +10,7 @@ using Volo.Abp.ObjectExtending; using Volo.Abp.Users; using Volo.CmsKit.Admin.MediaDescriptors; using Volo.CmsKit.Blogs; +using Volo.CmsKit.Comments; using Volo.CmsKit.Features; using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.Permissions; @@ -25,6 +26,8 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe protected BlogPostManager BlogPostManager { get; } protected IBlogPostRepository BlogPostRepository { get; } protected IBlogRepository BlogRepository { get; } + + protected ICommentRepository CommentRepository { get; } protected ICmsUserLookupService UserLookupService { get; } protected IMediaDescriptorAdminAppService MediaDescriptorAdminAppService { get; } @@ -34,13 +37,15 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe IBlogPostRepository blogPostRepository, IBlogRepository blogRepository, ICmsUserLookupService userLookupService, - IMediaDescriptorAdminAppService mediaDescriptorAdminAppService) + IMediaDescriptorAdminAppService mediaDescriptorAdminAppService, + ICommentRepository commentRepository) { BlogPostManager = blogPostManager; BlogPostRepository = blogPostRepository; BlogRepository = blogRepository; UserLookupService = userLookupService; MediaDescriptorAdminAppService = mediaDescriptorAdminAppService; + CommentRepository = commentRepository; } [Authorize(CmsKitAdminPermissions.BlogPosts.Create)] @@ -106,9 +111,15 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe { var blogs = (await BlogRepository.GetListAsync()).ToDictionary(x => x.Id); - var blogPosts = await BlogPostRepository.GetListAsync(input.Filter, input.BlogId, input.AuthorId, input.TagId, + var blogPosts = await BlogPostRepository.GetListAsync( + input.Filter, + input.BlogId, + input.AuthorId, + input.TagId, statusFilter: input.Status, - input.MaxResultCount, input.SkipCount, input.Sorting); + maxResultCount: input.MaxResultCount, + skipCount: input.SkipCount, + sorting: input.Sorting); var count = await BlogPostRepository.GetCountAsync(input.Filter, input.BlogId, input.AuthorId, tagId: input.TagId); @@ -126,7 +137,7 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe [Authorize(CmsKitAdminPermissions.BlogPosts.Delete)] public virtual async Task DeleteAsync(Guid id) { - await BlogPostRepository.DeleteAsync(id); + await BlogPostManager.DeleteAsync(id); } [Authorize(CmsKitAdminPermissions.BlogPosts.Publish)] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs index a6a0d6512a..60f8ccb8a8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs @@ -38,7 +38,7 @@ public class CmsKitAdminApplicationAutoMapperProfile : Profile CreateMap(MemberList.Source).MapExtraProperties(); CreateMap(MemberList.Source).MapExtraProperties(); - CreateMap().MapExtraProperties(); + CreateMap().Ignore(b => b.BlogPostCount).MapExtraProperties(); CreateMap(MemberList.Destination); diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs index 65952c1a15..13abdf9607 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Data; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; @@ -23,15 +25,18 @@ public class MenuItemAdminAppService : CmsKitAdminAppServiceBase, IMenuItemAdmin protected MenuItemManager MenuManager { get; } protected IMenuItemRepository MenuItemRepository { get; } protected IPageRepository PageRepository { get; } + protected IPermissionDefinitionManager PermissionDefinitionManager { get; } public MenuItemAdminAppService( MenuItemManager menuManager, IMenuItemRepository menuRepository, - IPageRepository pageRepository) + IPageRepository pageRepository, + IPermissionDefinitionManager permissionDefinitionManager) { MenuManager = menuManager; MenuItemRepository = menuRepository; PageRepository = pageRepository; + PermissionDefinitionManager = permissionDefinitionManager; } public virtual async Task> GetListAsync() @@ -70,7 +75,8 @@ public class MenuItemAdminAppService : CmsKitAdminAppServiceBase, IMenuItemAdmin input.Target, input.ElementId, input.CssClass, - CurrentTenant.Id + CurrentTenant.Id, + input.RequiredPermissionName ); if (input.PageId.HasValue) @@ -103,6 +109,7 @@ public class MenuItemAdminAppService : CmsKitAdminAppServiceBase, IMenuItemAdmin menuItem.Target = input.Target; menuItem.ElementId = input.ElementId; menuItem.CssClass = input.CssClass; + menuItem.RequiredPermissionName = input.RequiredPermissionName; menuItem.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp); input.MapExtraPropertiesTo(menuItem); await MenuItemRepository.UpdateAsync(menuItem); @@ -138,4 +145,21 @@ public class MenuItemAdminAppService : CmsKitAdminAppServiceBase, IMenuItemAdmin ObjectMapper.Map, List>(pages) ); } + + public virtual async Task> GetPermissionLookupAsync(PermissionLookupInputDto inputDto) + { + var permissions = await PermissionDefinitionManager.GetPermissionsAsync(); + + var permissionLookupDtos= permissions + .WhereIf(!inputDto.Filter.IsNullOrWhiteSpace(), p => p.Name.Contains(inputDto.Filter, StringComparison.OrdinalIgnoreCase)) + .Select(x => new PermissionLookupDto + { + Name = x.Name, + DisplayName = x.DisplayName.Localize(StringLocalizerFactory) + }).ToList(); + + return new ListResultDto( + permissionLookupDtos + ); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Pages/PageAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Pages/PageAdminAppService.cs index 1f5194c200..2860946490 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Pages/PageAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Pages/PageAdminAppService.cs @@ -8,6 +8,7 @@ using Volo.Abp.Data; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; using Volo.Abp.ObjectExtending; +using Volo.CmsKit.Comments; using Volo.CmsKit.Features; using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.Pages; @@ -21,6 +22,8 @@ namespace Volo.CmsKit.Admin.Pages; public class PageAdminAppService : CmsKitAdminAppServiceBase, IPageAdminAppService { protected IPageRepository PageRepository { get; } + + protected ICommentRepository CommentRepository { get; } protected PageManager PageManager { get; } @@ -29,11 +32,13 @@ public class PageAdminAppService : CmsKitAdminAppServiceBase, IPageAdminAppServi public PageAdminAppService( IPageRepository pageRepository, PageManager pageManager, - IDistributedCache pageCache) + IDistributedCache pageCache, + ICommentRepository commentRepository) { PageRepository = pageRepository; PageManager = pageManager; PageCache = pageCache; + CommentRepository = commentRepository; } public virtual async Task GetAsync(Guid id) @@ -108,6 +113,7 @@ public class PageAdminAppService : CmsKitAdminAppServiceBase, IPageAdminAppServi await PageRepository.DeleteAsync(page); await PageCache.RemoveAsync(PageCacheItem.GetKey(page.Slug)); + await CommentRepository.DeleteByEntityTypeAndIdAsync(PageConsts.EntityType, id.ToString()); } [Authorize(CmsKitAdminPermissions.Pages.SetAsHomePage)] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Blogs/BlogAdminClientProxy.Generated.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Blogs/BlogAdminClientProxy.Generated.cs index d102ea4d1f..7eb5d91f3b 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Blogs/BlogAdminClientProxy.Generated.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Blogs/BlogAdminClientProxy.Generated.cs @@ -57,4 +57,18 @@ public partial class BlogAdminClientProxy : ClientProxyBase> GetAllListAsync() + { + return await RequestAsync>(nameof(GetAllListAsync)); + } + + public virtual async Task MoveAllBlogPostsAsync(Guid blogId, Guid? assignToBlogId) + { + await RequestAsync(nameof(MoveAllBlogPostsAsync), new ClientProxyRequestTypeValue + { + { typeof(Guid), blogId }, + { typeof(Guid?), assignToBlogId } + }); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Menus/MenuItemAdminClientProxy.Generated.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Menus/MenuItemAdminClientProxy.Generated.cs index 70daab4da0..c707d6ad64 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Menus/MenuItemAdminClientProxy.Generated.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/Volo/CmsKit/Admin/Menus/MenuItemAdminClientProxy.Generated.cs @@ -72,4 +72,12 @@ public partial class MenuItemAdminClientProxy : ClientProxyBase> GetPermissionLookupAsync(PermissionLookupInputDto inputDto) + { + return await RequestAsync>(nameof(GetPermissionLookupAsync), new ClientProxyRequestTypeValue + { + { typeof(PermissionLookupInputDto), inputDto } + }); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json index cf4bbd1034..9e5f9a843e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi.Client/ClientProxies/cms-kit-admin-generate-proxy.json @@ -16,6 +16,39 @@ "type": "Volo.CmsKit.Admin.Blogs.IBlogAdminAppService", "name": "IBlogAdminAppService", "methods": [ + { + "name": "GetAllListAsync", + "parametersOnMethod": [], + "returnValue": { + "type": "Volo.Abp.Application.Dtos.ListResultDto", + "typeSimple": "Volo.Abp.Application.Dtos.ListResultDto" + } + }, + { + "name": "MoveAllBlogPostsAsync", + "parametersOnMethod": [ + { + "name": "blogId", + "typeAsString": "System.Guid, System.Private.CoreLib", + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null + }, + { + "name": "assignToBlogId", + "typeAsString": "System.Nullable`1[[System.Guid, System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib", + "type": "System.Guid?", + "typeSimple": "string?", + "isOptional": true, + "defaultValue": null + } + ], + "returnValue": { + "type": "System.Void", + "typeSimple": "System.Void" + } + }, { "name": "GetAsync", "parametersOnMethod": [ @@ -353,6 +386,90 @@ }, "allowAnonymous": false, "implementFrom": "Volo.Abp.Application.Services.IDeleteAppService" + }, + "GetAllListAsync": { + "uniqueName": "GetAllListAsync", + "name": "GetAllListAsync", + "httpMethod": "GET", + "url": "api/cms-kit-admin/blogs/all", + "supportedVersions": [], + "parametersOnMethod": [], + "parameters": [], + "returnValue": { + "type": "Volo.Abp.Application.Dtos.ListResultDto", + "typeSimple": "Volo.Abp.Application.Dtos.ListResultDto" + }, + "allowAnonymous": false, + "implementFrom": "Volo.CmsKit.Admin.Blogs.IBlogAdminAppService" + }, + "MoveAllBlogPostsAsyncByBlogIdAndAssignToBlogId": { + "uniqueName": "MoveAllBlogPostsAsyncByBlogIdAndAssignToBlogId", + "name": "MoveAllBlogPostsAsync", + "httpMethod": "PUT", + "url": "api/cms-kit-admin/blogs/{id}/move-all-blog-posts", + "supportedVersions": [], + "parametersOnMethod": [ + { + "name": "blogId", + "typeAsString": "System.Guid, System.Private.CoreLib", + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null + }, + { + "name": "assignToBlogId", + "typeAsString": "System.Nullable`1[[System.Guid, System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib", + "type": "System.Guid?", + "typeSimple": "string?", + "isOptional": false, + "defaultValue": null + } + ], + "parameters": [ + { + "nameOnMethod": "blogId", + "name": "blogId", + "jsonName": null, + "type": "System.Guid", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "ModelBinding", + "descriptorName": "" + }, + { + "nameOnMethod": "assignToBlogId", + "name": "assignToBlogId", + "jsonName": null, + "type": "System.Guid?", + "typeSimple": "string?", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "Query", + "descriptorName": "" + }, + { + "nameOnMethod": "id", + "name": "id", + "jsonName": null, + "type": null, + "typeSimple": null, + "isOptional": false, + "defaultValue": null, + "constraintTypes": [], + "bindingSourceId": "Path", + "descriptorName": "" + } + ], + "returnValue": { + "type": "System.Void", + "typeSimple": "System.Void" + }, + "allowAnonymous": false, + "implementFrom": "Volo.CmsKit.Admin.Blogs.IBlogAdminAppService" } } }, @@ -2050,6 +2167,23 @@ "type": "Volo.Abp.Application.Dtos.PagedResultDto", "typeSimple": "Volo.Abp.Application.Dtos.PagedResultDto" } + }, + { + "name": "GetPermissionLookupAsync", + "parametersOnMethod": [ + { + "name": "inputDto", + "typeAsString": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto, Volo.CmsKit.Admin.Application.Contracts", + "type": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto", + "typeSimple": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto", + "isOptional": false, + "defaultValue": null + } + ], + "returnValue": { + "type": "Volo.Abp.Application.Dtos.ListResultDto", + "typeSimple": "Volo.Abp.Application.Dtos.ListResultDto" + } } ] } @@ -2367,6 +2501,43 @@ }, "allowAnonymous": false, "implementFrom": "Volo.CmsKit.Admin.Menus.IMenuItemAdminAppService" + }, + "GetPermissionLookupAsyncByInputDto": { + "uniqueName": "GetPermissionLookupAsyncByInputDto", + "name": "GetPermissionLookupAsync", + "httpMethod": "GET", + "url": "api/cms-kit-admin/menu-items/lookup/permissions", + "supportedVersions": [], + "parametersOnMethod": [ + { + "name": "inputDto", + "typeAsString": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto, Volo.CmsKit.Admin.Application.Contracts", + "type": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto", + "typeSimple": "Volo.CmsKit.Admin.Menus.PermissionLookupInputDto", + "isOptional": false, + "defaultValue": null + } + ], + "parameters": [ + { + "nameOnMethod": "inputDto", + "name": "Filter", + "jsonName": null, + "type": "System.String", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "ModelBinding", + "descriptorName": "inputDto" + } + ], + "returnValue": { + "type": "Volo.Abp.Application.Dtos.ListResultDto", + "typeSimple": "Volo.Abp.Application.Dtos.ListResultDto" + }, + "allowAnonymous": false, + "implementFrom": "Volo.CmsKit.Admin.Menus.IMenuItemAdminAppService" } } }, diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs index e7d59ddbcd..f90215ed38 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogAdminController.cs @@ -62,4 +62,19 @@ public class BlogAdminController : CmsKitAdminController, IBlogAdminAppService { return BlogAdminAppService.DeleteAsync(id); } + + [HttpGet] + [Route("all")] + public Task> GetAllListAsync() + { + return BlogAdminAppService.GetAllListAsync(); + } + + [HttpPut] + [Route("{id}/move-all-blog-posts")] + [Authorize(CmsKitAdminPermissions.Blogs.Delete)] + public Task MoveAllBlogPostsAsync(Guid blogId, [FromQuery]Guid? assignToBlogId) + { + return BlogAdminAppService.MoveAllBlogPostsAsync(blogId, assignToBlogId); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Menus/MenuItemAdminController.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Menus/MenuItemAdminController.cs index 8540e5306e..e3cac24f54 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Menus/MenuItemAdminController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Menus/MenuItemAdminController.cs @@ -78,4 +78,11 @@ public class MenuItemAdminController : CmsKitAdminController, IMenuItemAdminAppS { return MenuItemAdminAppService.GetPageLookupAsync(input); } + + [HttpGet] + [Route("lookup/permissions")] + public Task> GetPermissionLookupAsync(PermissionLookupInputDto inputDto) + { + return MenuItemAdminAppService.GetPermissionLookupAsync(inputDto); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml new file mode 100644 index 0000000000..5f32d8e9bf --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml @@ -0,0 +1,59 @@ +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@using Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs +@using Volo.CmsKit.Localization +@model DeleteBlogModal +@inject IHtmlLocalizer L +@{ + Layout = null; +} + +
    + @{ + var deleteAllClicked = "checked"; + var deleteButtonDisabled = ""; + + + + + + +

    @L.GetString("BlogDeletionConfirmationMessage", Model.Blog.Name).Value

    + + @if (Model.Blog.BlogPostCount > 0) + { +

    @L.GetString("ChooseAnActionForBlog", Model.Blog.BlogPostCount).Value

    + + + if (Model.Blog.OtherBlogs.Any()) + { + deleteAllClicked = ""; + deleteButtonDisabled = "disabled"; +
    + + +
    + + } + +
    + + +
    + } +
    + + + + +
    + } + +
    diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml.cs new file mode 100644 index 0000000000..08e320b6f5 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/DeleteBlogModal.cshtml.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Volo.Abp.ObjectExtending; +using Volo.CmsKit.Admin.Blogs; + +namespace Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs; + +public class DeleteBlogModal : CmsKitAdminPageModel +{ + [BindProperty] + public BlogInfoModel Blog { get; set; } + + protected IBlogAdminAppService BlogAdminAppService { get; } + + public DeleteBlogModal(IBlogAdminAppService blogAdminAppService) + { + BlogAdminAppService = blogAdminAppService; + } + + public virtual async Task OnGetAsync(Guid id) + { + var blog = await BlogAdminAppService.GetAsync(id); + var allBlogs = await BlogAdminAppService.GetAllListAsync(); + + Blog = new BlogInfoModel + { + Id = blog.Id, + Name = blog.Name, + BlogPostCount = blog.BlogPostCount, + OtherBlogs = allBlogs.Items.Where(b => b.Id != blog.Id).Select(e => new KeyValuePair(e.Id, e.Name)).ToList() + }; + } + + public virtual async Task OnPostAsync() + { + await BlogAdminAppService.MoveAllBlogPostsAsync(Blog.Id, Blog.AssignToBlogId); + await BlogAdminAppService.DeleteAsync(Blog.Id); + return NoContent(); + } + + public class BlogInfoModel : ExtensibleObject + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public int BlogPostCount { get; set; } + + public List> OtherBlogs { get; set; } + + public Guid? AssignToBlogId { get; set; } + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/index.js b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/index.js index 0ead857f17..eef9af3f09 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/index.js +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/index.js @@ -6,6 +6,35 @@ $(function () { var updateModal = new abp.ModalManager({ viewUrl: abp.appPath + "CmsKit/Blogs/UpdateModal", modalClass: 'updateBlog' }); var featuresModal = new abp.ModalManager(abp.appPath + "CmsKit/Blogs/FeaturesModal"); + + var deleteBlogModal = new abp.ModalManager(abp.appPath + 'CmsKit/Blogs/DeleteBlogModal'); + + deleteBlogModal.onResult(function(){ + abp.notify.success(l('DeletedSuccessfully')); + }); + + deleteBlogModal.onOpen(function () { + var $form = deleteBlogModal.getForm(); + $form.find('#assign').click(function () { + $form.find('#Blog_AssignToBlogId').show(); + $form.find('[type=submit]').attr("disabled","disabled") + }) + $form.find('#deleteAll').click(function () { + $form.find('#Blog_AssignToBlogId').hide(); + $form.find('#Blog_AssignToBlogId').val(""); + $form.find('[type=submit]').removeAttr("disabled"); + }) + + $("#Blog_AssignToBlogId").on("change", function () { + var val = $(this).val(); + if(val === ''){ + $form.find('[type=submit]').attr("disabled","disabled") + }else{ + $form.find('[type=submit]').removeAttr("disabled"); + } + }) + }) + var blogsService = volo.cmsKit.admin.blogs.blogAdmin; var dataTable = $("#BlogsTable").DataTable(abp.libs.datatables.normalizeConfiguration({ @@ -41,16 +70,10 @@ $(function () { { text: l('Delete'), visible: abp.auth.isGranted('CmsKit.Blogs.Delete'), - confirmMessage: function (data) { - return l("BlogDeletionConfirmationMessage", data.record.name) - }, action: function (data) { - blogsService - .delete(data.record.id) - .then(function () { - dataTable.ajax.reloadEx(); - abp.notify.success(l('DeletedSuccessfully')); - }); + deleteBlogModal.open({ + id: data.record.id + }); } } ] @@ -74,6 +97,7 @@ $(function () { createModal.open(); }); + createModal.onResult(function () { dataTable.ajax.reloadEx(); }); @@ -81,4 +105,8 @@ $(function () { updateModal.onResult(function () { dataTable.ajax.reloadEx(); }); + + deleteBlogModal.onResult(function () { + dataTable.ajax.reloadEx(); + }); }); \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml index a79912e51c..8bce5cf45e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml @@ -60,6 +60,11 @@ +
    + + +
    +
    diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs index eaab7e8903..49a1354b43 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; using Volo.Abp.ObjectExtending; @@ -32,7 +33,6 @@ public class CreateModalModel : CmsKitAdminPageModel public virtual async Task OnGetAsync(Guid? parentId) { ViewModel.ParentId = parentId; - IsPageFeatureEnabled = GlobalFeatureManager.Instance.IsEnabled() && await FeatureChecker.IsEnabledAsync(CmsKitFeatures.PageEnable); } @@ -72,6 +72,8 @@ public class CreateModalModel : CmsKitAdminPageModel public string ElementId { get; set; } public string CssClass { get; set; } + + public string RequiredPermissionName { get; set; } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml index a190808560..48e37b6468 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml @@ -64,6 +64,30 @@ } +
    + + +
    +
    diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs index b64427e516..9cb5af1bba 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using AutoMapper; @@ -28,6 +29,8 @@ public class UpdateModalModel : CmsKitAdminPageModel public Guid Id { get; set; } public bool IsPageFeatureEnabled { get; set; } + + public IReadOnlyList Permissions { get; set; } public UpdateModalModel(IMenuItemAdminAppService menuAdminAppService, IFeatureChecker featureChecker) { @@ -39,7 +42,7 @@ public class UpdateModalModel : CmsKitAdminPageModel public async Task OnGetAsync() { var menuItemDto = await MenuAdminAppService.GetAsync(Id); - + Permissions = (await MenuAdminAppService.GetPermissionLookupAsync(new PermissionLookupInputDto())).Items; IsPageFeatureEnabled = GlobalFeatureManager.Instance.IsEnabled() && await FeatureChecker.IsEnabledAsync(CmsKitFeatures.PageEnable); @@ -76,6 +79,8 @@ public class UpdateModalModel : CmsKitAdminPageModel public Guid? PageId { get; set; } public string? PageTitle { get; set; } + + public string RequiredPermissionName { get; set; } [HiddenInput] public string ConcurrencyStamp { get; set; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/createModal.js b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/createModal.js index 0c4103c6f8..3459328956 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/createModal.js +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/createModal.js @@ -8,6 +8,7 @@ $(function () { var $url = $('#ViewModel_Url'); var $displayName = $('#ViewModel_DisplayName'); var $menuItemForm = $('#menu-item-form'); + var $selectRequiredPermission = $('#requiredPermissionName'); $pageId.on('change', function (params) { $url.prop('disabled', $pageId.val()); @@ -20,6 +21,51 @@ $(function () { } }) + function initSelectRequiredPermission(){ + function formatDisplayName(item) { + if (!item.id) { + return item.text; + } + var $displayName = $(`${item.id}`); + $displayName.tooltip(); + return $displayName; + } + + $selectRequiredPermission.select2({ + ajax:{ + url: '/api/cms-kit-admin/menu-items/lookup/permissions', + delay: 250, + dataType: "json", + data: function (params) { + let query = {}; + query["filter"] = params.term; + return query; + }, + processResults: function (data) { + let retVal = []; + let items = data["items"]; + $('body').tooltip('dispose'); + items.forEach(function (item, index) { + retVal.push({ + id: item["name"], + text: item["name"], + displayName: item["displayName"] + }) + }); + return { + results: retVal + }; + } + }, + templateResult: formatDisplayName, + width: '100%', + dropdownParent: $('#menu-create-modal'), + language: abp.localization.currentCulture.cultureName + }); + } + + initSelectRequiredPermission(); + $menuItemForm.on('submit', function (e) { $('[href="#url"]').tab('show'); }); diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/updateModal.js b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/updateModal.js index 173c6459e5..eb15bf7733 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/updateModal.js +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/updateModal.js @@ -8,6 +8,7 @@ $(function () { var $url = $('#ViewModel_Url'); var $displayName = $('#ViewModel_DisplayName'); var $menuItemForm = $('#menu-item-form'); + var $selectRequiredPermission = $('#requiredPermissionName'); $pageId.on('change', function (params) { $url.prop('disabled', $pageId.val()); @@ -21,6 +22,52 @@ $(function () { $pageId.trigger('change'); + function initSelectRequiredPermission(){ + function formatDisplayName(item) { + if (!item.id) { + return item.text; + } + var $displayName = $(`${item.id}`); + $displayName.tooltip(); + return $displayName; + } + + $selectRequiredPermission.select2({ + ajax:{ + url: '/api/cms-kit-admin/menu-items/lookup/permissions', + delay: 250, + dataType: "json", + data: function (params) { + let query = {}; + query["filter"] = params.term; + return query; + }, + processResults: function (data) { + let retVal = []; + let items = data["items"]; + $('body').tooltip('dispose'); + items.forEach(function (item, index) { + retVal.push({ + id: item["name"], + text: item["name"], + displayName: item["displayName"] + }) + }); + return { + results: retVal + }; + } + }, + templateResult: formatDisplayName, + width: '100%', + dropdownParent: $('#menu-update-modal'), + language: abp.localization.currentCulture.cultureName + }); + } + + initSelectRequiredPermission(); + $('[data-ts-toggle="tooltip"]').tooltip() + $menuItemForm.on('submit', function (e) { $('[href="#url"]').tab('show'); }); diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application.Contracts/Volo/CmsKit/Menus/MenuItemDto.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Application.Contracts/Volo/CmsKit/Menus/MenuItemDto.cs index 3d785cb9f1..b913f8b49e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application.Contracts/Volo/CmsKit/Menus/MenuItemDto.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application.Contracts/Volo/CmsKit/Menus/MenuItemDto.cs @@ -26,6 +26,8 @@ public class MenuItemDto : ExtensibleAuditedEntityDto, IHasConcurrencyStam public string CssClass { get; set; } public Guid? PageId { get; set; } + + public string RequiredPermissionName { get; set; } public string ConcurrencyStamp { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ar.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ar.json index c86c314d87..c67dae4a57 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ar.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ar.json @@ -15,6 +15,7 @@ "CmsKit.Ratings": "التقييمات", "CmsKit.Reactions": "تفاعلات", "CmsKit.Tags": "العلامات", + "CmsKit.MarkedItems": "العناصر المميزة", "CmsKit:0002": "المحتوى موجود بالفعل!", "CmsKit:0003": "الكيان {0} غير قابل للعلامة.", "CmsKit:Blog:0001": "slug المحدد ({Slug}) موجود بالفعل!", @@ -28,6 +29,9 @@ "CmsKit:MarkedItem:0001": "لا يمكن أن تمييز الكيان {EntityType}.", "CmsKit:MarkedItem:0002": "لم يتم العثور على تعريف لنوع الكيان '{EntityType}'.", "CmsKit:MarkedItem:0003": "يوجد بالفعل تعريف لنوع الكيان '{EntityType}'. يجب أن يكون لكل نوع كيان تعريف واحد فقط.", + "ToggleFavorite": "تفضيل/إلغاء", + "FavoritesFilterMessage": "يرجى تسجيل الدخول لتصفية المفضلات الخاصة بك", + "FilterOnFavorites": "تصفية المفضلات", "CmsKit:Tag:0002": "الكيان غير قابل للوسم!", "CommentAuthorizationExceptionMessage": "هذه التعليقات غير مسموح بها للعرض العام.", "CommentDeletionConfirmationMessage": "سيتم حذف هذا التعليق وجميع الردود!", @@ -237,6 +241,11 @@ "CssClass": "فئة CSS", "TagsHelpText": "يجب أن تكون العلامات مفصولة بفواصل (على سبيل المثال: tag1، tag2، tag3)", "ThisPartOfContentCouldntBeLoaded": "لا يمكن تحميل هذا الجزء من المحتوى.", - "DuplicateCommentAttemptMessage": "تم اكتشاف محاولة نشر تعليق مكررة. لقد تم بالفعل تقديم تعليقك." + "DuplicateCommentAttemptMessage": "تم اكتشاف محاولة نشر تعليق مكررة. لقد تم بالفعل تقديم تعليقك.", + "ChooseAnActionForBlog": "اختر إجراءً للمدونة", + "AssignBlogPostsToOtherBlog": "تعيين مشاركات المدونة إلى مدونة أخرى", + "SelectAnBlogToAssign": "حدد مدونة لتعيين مشاركات المدونة إليها", + "DeleteAllBlogPostsOfThisBlog": "حذف جميع مشاركات المدونة", + "RequiredPermissionName": "اسم الإذن المطلوب", } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json index c4534837fa..b0c4863bdf 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json @@ -227,6 +227,11 @@ "CssClass": "Třída CSS", "TagsHelpText": "Značky by měly být odděleny čárkami (např.: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Tato část obsahu nemohla být načtena.", - "DuplicateCommentAttemptMessage": "Byl zjištěn duplicitní pokus o vložení komentáře. Váš komentář již byl odeslán." + "DuplicateCommentAttemptMessage": "Byl zjištěn duplicitní pokus o vložení komentáře. Váš komentář již byl odeslán.", + "ChooseAnActionForBlog": "Vyberte akci pro blog", + "AssignBlogPostsToOtherBlog": "Přiřaďte blogové příspěvky k jinému blogu", + "SelectAnBlogToAssign": "Vyberte blog, ke kterému chcete přiřadit blogové příspěvky", + "DeleteAllBlogPostsOfThisBlog": "Smazat všechny blogové příspěvky tohoto blogu", + "RequiredPermissionName": "Je vyžadováno oprávnění" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de-DE.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de-DE.json index 98b8b731c4..767c47497d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de-DE.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de-DE.json @@ -161,6 +161,11 @@ "YourFullName": "Ihren vollständigen Namen", "YourMessage": "Ihre Nachricht", "YourReply": "Ihre Antwort", - "ThisPartOfContentCouldntBeLoaded": "Dieser Teil des Inhalts konnte nicht geladen werden." + "ThisPartOfContentCouldntBeLoaded": "Dieser Teil des Inhalts konnte nicht geladen werden.", + "ChooseAnActionForBlog": "Wählen Sie eine Aktion für den Blog", + "AssignBlogPostsToOtherBlog": "Blogbeiträge einem anderen Blog zuweisen", + "SelectAnBlogToAssign": "Wählen Sie einen Blog aus, um Blogbeiträge zuzuweisen", + "DeleteAllBlogPostsOfThisBlog": "Alle Blogbeiträge dieses Blogs löschen", + "RequiredPermissionName": "Erforderlicher Berechtigungsname" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de.json index 31bae240c3..6ee49f4675 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/de.json @@ -227,6 +227,11 @@ "CssClass": "CSS-Klasse", "TagsHelpText": "Tags sollten durch Kommas getrennt werden (z. B. Tag1, Tag2, Tag3).", "ThisPartOfContentCouldntBeLoaded": "Dieser Teil des Inhalts konnte nicht geladen werden.", - "DuplicateCommentAttemptMessage": "Es wurde versucht, einen doppelten Kommentar zu posten. Ihr Kommentar wurde bereits übermittelt." + "DuplicateCommentAttemptMessage": "Es wurde versucht, einen doppelten Kommentar zu posten. Ihr Kommentar wurde bereits übermittelt.", + "ChooseAnActionForBlog": "Wählen Sie eine Aktion für den Blog", + "AssignBlogPostsToOtherBlog": "Blogbeiträge einem anderen Blog zuweisen", + "SelectAnBlogToAssign": "Wählen Sie einen Blog aus, um Blogbeiträge zuzuweisen", + "DeleteAllBlogPostsOfThisBlog": "Alle Blogbeiträge dieses Blogs löschen", + "RequiredPermissionName": "Erforderlicher Berechtigungsname" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/el.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/el.json index 695cdd21eb..af12ccfdf8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/el.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/el.json @@ -184,6 +184,11 @@ "SelectAStatus": "Επιλέξτε μια κατάσταση", "Status": "Κατάσταση", "CmsKit.BlogPost.ScrollIndex": "Γραμμή γρήγορης πλοήγησης σε αναρτήσεις ιστολογίου", - "ThisPartOfContentCouldntBeLoaded": "Αυτό το τμήμα του περιεχομένου δεν μπορούσε να φορτωθεί." + "ThisPartOfContentCouldntBeLoaded": "Αυτό το τμήμα του περιεχομένου δεν μπορούσε να φορτωθεί.", + "ChooseAnActionForBlog": "Επιλέξτε μια ενέργεια για το ιστολόγιο", + "AssignBlogPostsToOtherBlog": "Ανάθεση αναρτήσεων ιστολογίου σε άλλο ιστολόγιο", + "SelectAnBlogToAssign": "Επιλέξτε ένα ιστολόγιο για να αναθέσετε αναρτήσεις ιστολογίου", + "DeleteAllBlogPostsOfThisBlog": "Διαγραφή όλων των αναρτήσεων ιστολογίου αυτού του ιστολογίου", + "RequiredPermissionName": "Απαιτούμενο όνομα δικαιώματος" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en-GB.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en-GB.json index f6256d81ba..76125e555a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en-GB.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en-GB.json @@ -28,6 +28,11 @@ "Undo": "Undo", "Update": "Update", "YourComment": "Your comment", - "YourReply": "Your reply" + "YourReply": "Your reply", + "ChooseAnActionForBlog": "Choose an action for the blog", + "AssignBlogPostsToOtherBlog": "Assign blog posts to another blog", + "SelectAnBlogToAssign": "Select a blog to assign", + "DeleteAllBlogPostsOfThisBlog": "Delete all blog posts of this blog", + "RequiredPermissionName": "Required permission name" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json index 149f1072bf..4fcbc946c9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json @@ -15,6 +15,7 @@ "CmsKit.Ratings": "Ratings", "CmsKit.Reactions": "Reactions", "CmsKit.Tags": "Tags", + "CmsKit.MarkedItems": "Marked Items", "CmsKit:0002": "Content already exists!", "CmsKit:0003": "The entity {0} is not taggable.", "CmsKit:Blog:0001": "The given slug ({Slug}) already exists!", @@ -27,6 +28,9 @@ "CmsKit:Reaction:0001": "The entity {EntityType} can't have reactions.", "CmsKit:Tag:0002": "The entity is not taggable!", "CmsKit:MarkedItem:ToggleConfirmation": "Are you sure you want to toggle the marked item?", + "ToggleFavorite": "Add/Remove Favorite", + "FavoritesFilterMessage": "Please log in to filter your favorites", + "FilterOnFavorites": "Filter On Favorites", "CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display.", "CmsKit:Modals:Login": "Login", "CmsKit:Modals:LoginModalDefaultMessage": "Please login to continue!", @@ -255,6 +259,11 @@ "CommentAlertMessage": "There are {0} comments waiting for approval", "Settings:Menu:CmsKit": "CMS", "CommentsAwaitingApproval": "Comments Awaiting Approval", - "CommentSubmittedForApproval": "Your comment has been submitted for approval." + "CommentSubmittedForApproval": "Your comment has been submitted for approval.", + "ChooseAnActionForBlog": "Choose an action for the blog", + "AssignBlogPostsToOtherBlog": "Assign blog posts to another blog", + "SelectAnBlogToAssign": "Select a blog to assign", + "DeleteAllBlogPostsOfThisBlog": "Delete all blog posts of this blog", + "RequiredPermissionName": "Required permission name" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/es.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/es.json index e1ff8d7fc8..985ccd10d5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/es.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/es.json @@ -227,6 +227,11 @@ "CssClass": "Clase CSS", "TagsHelpText": "Las etiquetas deben estar separadas por comas (por ejemplo: etiqueta1, etiqueta2, etiqueta3)", "ThisPartOfContentCouldntBeLoaded": "Esta parte del contenido no se pudo cargar.", - "DuplicateCommentAttemptMessage": "Intento de publicación de comentario duplicado detectado. Tu comentario ya ha sido enviado." + "DuplicateCommentAttemptMessage": "Intento de publicación de comentario duplicado detectado. Tu comentario ya ha sido enviado.", + "ChooseAnActionForBlog": "Elija una acción para el blog", + "AssignBlogPostsToOtherBlog": "Asignar publicaciones de blog a otro blog", + "SelectAnBlogToAssign": "Seleccione un blog para asignar publicaciones de blog", + "DeleteAllBlogPostsOfThisBlog": "Eliminar todas las publicaciones de blog de este blog", + "RequiredPermissionName": "Nombre de permiso requerido" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fa.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fa.json index e044cde1a5..da26b16bd7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fa.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fa.json @@ -183,6 +183,11 @@ "Status": "وضعیت", "CmsKit.BlogPost.ScrollIndex": "نوار پیمایش سریع در پست های وبلاگ", "CmsKit.BlogPost.PreventXssFeature": "جلوگیری از XSS", - "ThisPartOfContentCouldntBeLoaded": "این قسمت از محتوا قابل بارگیری نیست." + "ThisPartOfContentCouldntBeLoaded": "این قسمت از محتوا قابل بارگیری نیست.", + "ChooseAnActionForBlog": "یک عمل برای وبلاگ انتخاب کنید", + "AssignBlogPostsToOtherBlog": "پست های وبلاگ را به وبلاگ دیگری اختصاص دهید", + "SelectAnBlogToAssign": "یک وبلاگ برای اختصاص دادن انتخاب کنید", + "DeleteAllBlogPostsOfThisBlog": "تمام پست های وبلاگ این وبلاگ را حذف کنید", + "RequiredPermissionName": "نام مجوز مورد نیاز" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fi.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fi.json index 82d622f05f..637672dbdd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fi.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fi.json @@ -227,6 +227,11 @@ "CssClass": "CSS-luokka", "TagsHelpText": "Tunnisteet on erotettava pilkuilla (esim. tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Tämä sisällön osa ei voitu ladata.", - "DuplicateCommentAttemptMessage": "Kopiokommenttiyritys havaittiin. Kommenttisi on jo lähetetty." + "DuplicateCommentAttemptMessage": "Kopiokommenttiyritys havaittiin. Kommenttisi on jo lähetetty.", + "ChooseAnActionForBlog": "Valitse toiminto blogille", + "AssignBlogPostsToOtherBlog": "Määritä blogiviestit toiseen blogiin", + "SelectAnBlogToAssign": "Valitse blogi, johon haluat määrittää", + "DeleteAllBlogPostsOfThisBlog": "Poista tämän blogin kaikki blogiviestit", + "RequiredPermissionName": "Tarvittava lupa" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fr.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fr.json index 634c1fec85..805a9eeed1 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fr.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/fr.json @@ -227,6 +227,11 @@ "CssClass": "Classe CSS", "TagsHelpText": "Les balises doivent être séparées par des virgules (par exemple : tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Cette partie du contenu n'a pas pu être chargée.", - "DuplicateCommentAttemptMessage": "Tentative de publication de commentaire en double détectée. Votre commentaire a déjà été soumis." + "DuplicateCommentAttemptMessage": "Tentative de publication de commentaire en double détectée. Votre commentaire a déjà été soumis.", + "ChooseAnActionForBlog": "Choisissez une action pour le blog", + "AssignBlogPostsToOtherBlog": "Attribuer des articles de blog à un autre blog", + "SelectAnBlogToAssign": "Sélectionnez un blog à attribuer", + "DeleteAllBlogPostsOfThisBlog": "Supprimer tous les articles de blog de ce blog", + "RequiredPermissionName": "Nom de permission requis" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hi.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hi.json index dcea398df5..e5c0f008f5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hi.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hi.json @@ -227,6 +227,11 @@ "CssClass": "सीएसएस क्लास", "TagsHelpText": "टैग को अल्पविराम से अलग किया जाना चाहिए (जैसे: टैग1, टैग2, टैग3)", "ThisPartOfContentCouldntBeLoaded": "यह भाग लोड नहीं किया जा सका।", - "DuplicateCommentAttemptMessage": "डुप्लिकेट टिप्पणी पोस्ट प्रयास का पता चला। आपकी टिप्पणी पहले ही सबमिट की जा चुकी है." + "DuplicateCommentAttemptMessage": "डुप्लिकेट टिप्पणी पोस्ट प्रयास का पता चला। आपकी टिप्पणी पहले ही सबमिट की जा चुकी है.", + "ChooseAnActionForBlog": "ब्लॉग के लिए कोई कार्रवाई चुनें", + "AssignBlogPostsToOtherBlog": "अन्य ब्लॉग को ब्लॉग पोस्ट असाइन करें", + "SelectAnBlogToAssign": "असाइन करने के लिए एक ब्लॉग चुनें", + "DeleteAllBlogPostsOfThisBlog": "इस ब्लॉग के सभी ब्लॉग पोस्ट हटाएं", + "RequiredPermissionName": "आवश्यक अनुमति नाम" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hr.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hr.json index d2dff83aee..8530b5bfc9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hr.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hr.json @@ -227,6 +227,11 @@ "CssClass": "CSS klasa", "TagsHelpText": "Oznake trebaju biti odvojene zarezom (npr. oznaka1, oznaka2, oznaka3)", "ThisPartOfContentCouldntBeLoaded": "Ovaj dio sadržaja nije bilo moguće učitati.", - "DuplicateCommentAttemptMessage": "Otkriven pokušaj postavljanja duplikata komentara. Vaš komentar je već poslan." + "DuplicateCommentAttemptMessage": "Otkriven pokušaj postavljanja duplikata komentara. Vaš komentar je već poslan.", + "ChooseAnActionForBlog": "Odaberite radnju za blog", + "AssignBlogPostsToOtherBlog": "Dodijelite postove na blogu drugom blogu", + "SelectAnBlogToAssign": "Odaberite blog za dodjelu", + "DeleteAllBlogPostsOfThisBlog": "Izbrišite sve postove na blogu", + "RequiredPermissionName": "Potrebno ime dozvole" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hu.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hu.json index 219d9f95a7..52fedf88cd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hu.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/hu.json @@ -227,6 +227,11 @@ "CssClass": "CSS osztály", "TagsHelpText": "A címkéket vesszővel kell elválasztani (pl.: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "A tartalom ezen része nem tölthető be.", - "DuplicateCommentAttemptMessage": "Ismétlődő megjegyzés közzétételi kísérlet észlelve. Megjegyzését már elküldtük." + "DuplicateCommentAttemptMessage": "Ismétlődő megjegyzés közzétételi kísérlet észlelve. Megjegyzését már elküldtük.", + "ChooseAnActionForBlog": "Válasszon egy műveletet a bloghoz", + "AssignBlogPostsToOtherBlog": "Blogbejegyzések hozzárendelése egy másik bloghoz", + "SelectAnBlogToAssign": "Válasszon egy blogot a hozzárendeléshez", + "DeleteAllBlogPostsOfThisBlog": "Ez a művelet törli az összes blogbejegyzést ebből a blogból. Biztos vagy benne?", + "RequiredPermissionName": "Szükséges engedély neve" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/is.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/is.json index 6788847dd7..cfc4032dcc 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/is.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/is.json @@ -227,6 +227,11 @@ "CssClass": "CSS flokkur", "TagsHelpText": "Merki ættu að vera aðskilin með kommum (td: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Þessi hluti af efni gat ekki verið hlaðið inn.", - "DuplicateCommentAttemptMessage": "Tvítekin tilraun til að skrifa athugasemd fannst. Athugasemd þín hefur þegar verið send." + "DuplicateCommentAttemptMessage": "Tvítekin tilraun til að skrifa athugasemd fannst. Athugasemd þín hefur þegar verið send.", + "ChooseAnActionForBlog": "Veldu aðgerð fyrir blogg", + "AssignBlogPostsToOtherBlog": "Úthluta bloggfærslum til annars bloggs", + "SelectAnBlogToAssign": "Veldu blogg til að úthluta", + "DeleteAllBlogPostsOfThisBlog": "Eyða öllum bloggfærslum þessa bloggs", + "RequiredPermissionName": "Nafn á nauðsynlegri leyfi" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/it.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/it.json index c6d84a4f62..5e3e2d0937 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/it.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/it.json @@ -227,6 +227,11 @@ "CssClass": "Classe CSS", "TagsHelpText": "I tag devono essere separati da virgole (ad esempio: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Questa parte del contenuto non può essere caricata.", - "DuplicateCommentAttemptMessage": "Rilevato tentativo di pubblicare commenti duplicati. Il tuo commento è già stato inviato." + "DuplicateCommentAttemptMessage": "Rilevato tentativo di pubblicare commenti duplicati. Il tuo commento è già stato inviato.", + "ChooseAnActionForBlog": "Scegli un'azione per il blog", + "AssignBlogPostsToOtherBlog": "Assegna i post del blog ad un altro blog", + "SelectAnBlogToAssign": "Seleziona un blog a cui assegnare i post del blog", + "DeleteAllBlogPostsOfThisBlog": "Elimina tutti i post del blog di questo blog", + "RequiredPermissionName": "Nome del permesso richiesto" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/nl.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/nl.json index b7c275078a..8e4c97af34 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/nl.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/nl.json @@ -227,6 +227,11 @@ "CssClass": "CSS-klasse", "TagsHelpText": "Tags moeten door komma's worden gescheiden (bijvoorbeeld: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Dit deel van de inhoud kon niet worden geladen.", - "DuplicateCommentAttemptMessage": "Dubbele poging tot posten van commentaar gedetecteerd. Uw reactie is al verzonden." + "DuplicateCommentAttemptMessage": "Dubbele poging tot posten van commentaar gedetecteerd. Uw reactie is al verzonden.", + "ChooseAnActionForBlog": "Kies een actie voor de blog", + "AssignBlogPostsToOtherBlog": "Wijs blogberichten toe aan een andere blog", + "SelectAnBlogToAssign": "Selecteer een blog om toe te wijzen", + "DeleteAllBlogPostsOfThisBlog": "Verwijder alle blogberichten van deze blog", + "RequiredPermissionName": "Vereiste toestemming" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pl-PL.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pl-PL.json index 5237c0dc95..6ac083c3bc 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pl-PL.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pl-PL.json @@ -227,6 +227,11 @@ "CssClass": "Klasa CSS", "TagsHelpText": "Tagi powinny być oddzielone przecinkami (np. tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Ta część zawartości nie mogła zostać załadowana.", - "DuplicateCommentAttemptMessage": "Wykryto zduplikowaną próbę opublikowania komentarza. Twój komentarz został już przesłany." + "DuplicateCommentAttemptMessage": "Wykryto zduplikowaną próbę opublikowania komentarza. Twój komentarz został już przesłany.", + "ChooseAnActionForBlog": "Wybierz akcję dla bloga", + "AssignBlogPostsToOtherBlog": "Przypisz posty na blogu do innego bloga", + "SelectAnBlogToAssign": "Wybierz blog, do którego chcesz przypisać posty na blogu", + "DeleteAllBlogPostsOfThisBlog": "Usuń wszystkie posty na blogu tego bloga", + "RequiredPermissionName": "Wymagana nazwa uprawnienia" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pt-BR.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pt-BR.json index 3182469634..d557fca98e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pt-BR.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/pt-BR.json @@ -227,6 +227,11 @@ "CssClass": "Classe CSS", "TagsHelpText": "As tags devem ser separadas por vírgula (por exemplo: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Esta parte do conteúdo não pôde ser carregada.", - "DuplicateCommentAttemptMessage": "Tentativa duplicada de postagem de comentário detectada. Seu comentário já foi enviado." + "DuplicateCommentAttemptMessage": "Tentativa duplicada de postagem de comentário detectada. Seu comentário já foi enviado.", + "ChooseAnActionForBlog": "Escolha uma ação para o blog", + "AssignBlogPostsToOtherBlog": "Atribuir postagens de blog a outro blog", + "SelectAnBlogToAssign": "Selecione um blog para atribuir", + "DeleteAllBlogPostsOfThisBlog": "Excluir todas as postagens de blog deste blog", + "RequiredPermissionName": "Nome da permissão necessária" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ro-RO.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ro-RO.json index 7b9173f477..f37b7a0161 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ro-RO.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ro-RO.json @@ -227,6 +227,11 @@ "CssClass": "Clasa CSS", "TagsHelpText": "Etichetele ar trebui să fie separate prin virgulă (de exemplu: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Această parte a conţinutului nu a putut fi încărcată.", - "DuplicateCommentAttemptMessage": "A fost detectată o încercare de postare de comentarii duplicată. Comentariul dvs. a fost deja trimis." + "DuplicateCommentAttemptMessage": "A fost detectată o încercare de postare de comentarii duplicată. Comentariul dvs. a fost deja trimis.", + "ChooseAnActionForBlog": "Alegeţi o acţiune pentru blog", + "AssignBlogPostsToOtherBlog": "Atribuiţi postările de blog la alt blog", + "SelectAnBlogToAssign": "Selectaţi un blog pentru a atribui postările de blog", + "DeleteAllBlogPostsOfThisBlog": "Ştergeţi toate postările de blog ale acestui blog", + "RequiredPermissionName": "Numele permisiunii necesare" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ru.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ru.json index 491435bc02..7ff7851e73 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ru.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ru.json @@ -227,6 +227,11 @@ "CssClass": "CSS-класс", "TagsHelpText": "Теги должны быть разделены запятыми (например: тег1, тег2, тег3).", "ThisPartOfContentCouldntBeLoaded": "Эта часть содержимого не может быть загружена.", - "DuplicateCommentAttemptMessage": "Обнаружена повторная попытка публикации комментария. Ваш комментарий уже отправлен." + "DuplicateCommentAttemptMessage": "Обнаружена повторная попытка публикации комментария. Ваш комментарий уже отправлен.", + "ChooseAnActionForBlog": "Выберите действие для блога", + "AssignBlogPostsToOtherBlog": "Назначить сообщения в блоге другому блогу", + "SelectAnBlogToAssign": "Выберите блог для назначения", + "DeleteAllBlogPostsOfThisBlog": "Удалить все сообщения в блоге этого блога", + "RequiredPermissionName": "Имя требуемого разрешения" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sk.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sk.json index c579340750..6fadf538f0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sk.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sk.json @@ -227,6 +227,11 @@ "CssClass": "CSS trieda", "TagsHelpText": "Značky by mali byť oddelené čiarkou (napr.: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Táto časť obsahu sa nedá načítať.", - "DuplicateCommentAttemptMessage": "Zistil sa duplicitný pokus o uverejnenie komentára. Váš komentár už bol odoslaný." + "DuplicateCommentAttemptMessage": "Zistil sa duplicitný pokus o uverejnenie komentára. Váš komentár už bol odoslaný.", + "ChooseAnActionForBlog": "Vyberte akciu pre blog", + "AssignBlogPostsToOtherBlog": "Priradiť blogové príspevky k inému blogu", + "SelectAnBlogToAssign": "Vyberte blog, na ktorý chcete priradiť blogové príspevky", + "DeleteAllBlogPostsOfThisBlog": "Zmazať všetky blogové príspevky tohto blogu", + "RequiredPermissionName": "Požadovaný názov oprávnenia" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sl.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sl.json index 6e3157f56e..57a7c22f67 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sl.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sl.json @@ -227,6 +227,11 @@ "CssClass": "Razred CSS", "TagsHelpText": "Oznake morajo biti ločene z vejicami (npr. oznaka1, oznaka2, oznaka3)", "ThisPartOfContentCouldntBeLoaded": "Ta del vsebine ni bil naložen.", - "DuplicateCommentAttemptMessage": "Zaznan poskus podvojene objave komentarja. Vaš komentar je že oddan." + "DuplicateCommentAttemptMessage": "Zaznan poskus podvojene objave komentarja. Vaš komentar je že oddan.", + "ChooseAnActionForBlog": "Izberite dejanje za blog", + "AssignBlogPostsToOtherBlog": "Dodeli objave v blogu drugemu blogu", + "SelectAnBlogToAssign": "Izberite blog, ki mu želite dodeliti objave", + "DeleteAllBlogPostsOfThisBlog": "Izbriši vse objave v tem blogu", + "RequiredPermissionName": "Ime zahtevane dovoljenja" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sv.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sv.json index 6e8962915b..4d63eb822f 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sv.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/sv.json @@ -254,6 +254,11 @@ "CommentAlertMessage": "Det finns {0} kommentarer som väntar på godkännande", "Settings:Menu:CmsKit": "CMS", "CommentsAwaitingApproval": "Kommentarer som väntar på godkännande", - "CommentSubmittedForApproval": "Din kommentar har skickats in för godkännande." + "CommentSubmittedForApproval": "Din kommentar har skickats in för godkännande.", + "ChooseAnActionForBlog": "Välj en åtgärd för bloggen", + "AssignBlogPostsToOtherBlog": "Tilldela blogginlägg till en annan blogg", + "SelectAnBlogToAssign": "Välj en blogg att tilldela", + "DeleteAllBlogPostsOfThisBlog": "Radera alla blogginlägg i denna blogg", + "RequiredPermissionName": "Nödvändigt behörighetsnamn" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json index 3605cb9d3b..3e1166133c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json @@ -218,6 +218,11 @@ "CaptchaCode": "Captcha kodu", "CommentTextRequired": "Yorum zorunlu", "PopularTags": "Popüler Etiketler", - "ThisPartOfContentCouldntBeLoaded": "Bu içerik yüklenemedi" + "ThisPartOfContentCouldntBeLoaded": "Bu içerik yüklenemedi", + "ChooseAnActionForBlog": "Blog için bir eylem seçin", + "AssignBlogPostsToOtherBlog": "Diğer bloglara blog yazıları atayın", + "SelectAnBlogToAssign": "Atanacak bir blog seçin", + "DeleteAllBlogPostsOfThisBlog": "Bu blogun tüm blog yazılarını sil", + "RequiredPermissionName": "Gerekli izin adı" } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/vi.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/vi.json index 1ec430edc3..9d56cadb8f 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/vi.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/vi.json @@ -227,6 +227,11 @@ "CssClass": "Lớp CSS", "TagsHelpText": "Các thẻ phải được phân tách bằng dấu phẩy (ví dụ: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Phần này của nội dung không thể được tải.", - "DuplicateCommentAttemptMessage": "Đã phát hiện nỗ lực đăng bình luận trùng lặp. Bình luận của bạn đã được gửi." + "DuplicateCommentAttemptMessage": "Đã phát hiện nỗ lực đăng bình luận trùng lặp. Bình luận của bạn đã được gửi.", + "ChooseAnActionForBlog": "Chọn một hành động cho blog", + "AssignBlogPostsToOtherBlog": "Gán bài đăng trên blog cho blog khác", + "SelectAnBlogToAssign": "Chọn một blog để gán", + "DeleteAllBlogPostsOfThisBlog": "Xóa tất cả bài đăng trên blog của blog này", + "RequiredPermissionName": "Tên quyền cần thiết" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json index c02fcf614d..e3ad52a8ec 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json @@ -227,6 +227,11 @@ "CssClass": "CSS 类", "TagsHelpText": "标签应以逗号分隔(例如:标签 1,标签 2,标签 3)", "ThisPartOfContentCouldntBeLoaded": "这部分内容无法加载。", - "DuplicateCommentAttemptMessage": "检测到重复发表评论。您的评论已经提交。" + "DuplicateCommentAttemptMessage": "检测到重复发表评论。您的评论已经提交。", + "ChooseAnActionForBlog": "选择博客的操作", + "AssignBlogPostsToOtherBlog": "将博客文章分配给其他博客", + "SelectAnBlogToAssign": "选择要分配的博客", + "DeleteAllBlogPostsOfThisBlog": "删除此博客的所有博客文章", + "RequiredPermissionName": "所需权限名称" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json index 866fd3fcea..185fa3a3d5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json @@ -227,6 +227,11 @@ "CssClass": "CSS類", "TagsHelpText": "標籤應以逗號分隔(例如:tag1、tag2、tag3)", "ThisPartOfContentCouldntBeLoaded": "這部分內容無法加載", - "DuplicateCommentAttemptMessage": "偵測到重複的評論貼文嘗試。您的評論已經提交。" + "DuplicateCommentAttemptMessage": "偵測到重複的評論貼文嘗試。您的評論已經提交。", + "ChooseAnActionForBlog": "選擇部落格的操作", + "AssignBlogPostsToOtherBlog": "將部落格文章分配給其他部落格", + "SelectAnBlogToAssign": "選擇要分配的部落格", + "DeleteAllBlogPostsOfThisBlog": "刪除此部落格的所有部落格文章", + "RequiredPermissionName": "所需權限名稱" } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Menus/MenuItemConsts.cs b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Menus/MenuItemConsts.cs index 6484d5280e..25779876db 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Menus/MenuItemConsts.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Menus/MenuItemConsts.cs @@ -4,4 +4,5 @@ public static class MenuItemConsts { public const int MaxDisplayNameLength = 64; public const int MaxUrlLength = 1024; + public const int MaxRequiredPermissionNameLength = 128; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs index 3a4cf26c1b..a34a303008 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs @@ -53,7 +53,7 @@ public class BlogPost : FullAuditedAggregateRoot, IMultiTenant, IHasEntity ) : base(id) { TenantId = tenantId; - BlogId = blogId; + SetBlogId(blogId); AuthorId = authorId; SetTitle(title); SetSlug(slug); @@ -68,6 +68,11 @@ public class BlogPost : FullAuditedAggregateRoot, IMultiTenant, IHasEntity Title = Check.NotNullOrWhiteSpace(title, nameof(title), BlogPostConsts.MaxTitleLength); } + public virtual void SetBlogId(Guid blogId) + { + BlogId = blogId; + } + internal void SetSlug(string slug) { Check.NotNullOrWhiteSpace(slug, nameof(slug), BlogPostConsts.MaxSlugLength, BlogPostConsts.MinSlugLength); diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs index 9e21078707..029c8482ae 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Volo.Abp; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Services; +using Volo.CmsKit.Comments; using Volo.CmsKit.Users; namespace Volo.CmsKit.Blogs; @@ -12,17 +13,20 @@ public class BlogPostManager : DomainService { protected IBlogPostRepository BlogPostRepository { get; } protected IBlogRepository BlogRepository { get; } + protected ICommentRepository CommentRepository { get; } protected IDefaultBlogFeatureProvider BlogFeatureProvider { get; } public BlogPostManager( IBlogPostRepository blogPostRepository, IBlogRepository blogRepository, - IDefaultBlogFeatureProvider blogFeatureProvider) + IDefaultBlogFeatureProvider blogFeatureProvider, + ICommentRepository commentRepository) { BlogPostRepository = blogPostRepository; BlogRepository = blogRepository; BlogFeatureProvider = blogFeatureProvider; + CommentRepository = commentRepository; } public virtual async Task CreateAsync( @@ -66,6 +70,12 @@ public class BlogPostManager : DomainService blogPost.SetSlug(newSlug); } + + public virtual async Task DeleteAsync(Guid blogId) + { + await BlogPostRepository.DeleteAsync(blogId); + await CommentRepository.DeleteByEntityTypeAndIdAsync(BlogPostConsts.EntityType, blogId.ToString()); + } protected virtual async Task CheckSlugExistenceAsync(Guid blogId, string slug) { diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogWithBlogPostCount.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogWithBlogPostCount.cs new file mode 100644 index 0000000000..9a84402375 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogWithBlogPostCount.cs @@ -0,0 +1,14 @@ +namespace Volo.CmsKit.Blogs; + +public class BlogWithBlogPostCount +{ + public Blog Blog { get; set; } + + public int BlogPostCount { get; set; } + + public BlogWithBlogPostCount(Blog blog, int blogPostCount) + { + Blog = blog; + BlogPostCount = blogPostCount; + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/DefaultBlogFeatureProvider.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/DefaultBlogFeatureProvider.cs index b24e992cf3..dc74bd5ad6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/DefaultBlogFeatureProvider.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/DefaultBlogFeatureProvider.cs @@ -16,6 +16,7 @@ public class DefaultBlogFeatureProvider : IDefaultBlogFeatureProvider, ITransien new BlogFeature(blogId, ReactionsFeature.Name), new BlogFeature(blogId, RatingsFeature.Name), new BlogFeature(blogId, TagsFeature.Name), + new BlogFeature(blogId, MarkedItemsFeature.Name), new BlogFeature(blogId, BlogPostScrollIndexFeature.Name), new BlogFeature(blogId, BlogConsts.PreventXssFeatureName) }); diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs index 404d21dc83..e4d8980196 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs @@ -14,6 +14,7 @@ public interface IBlogPostRepository : IBasicRepository Guid? blogId = null, Guid? authorId = null, Guid? tagId = null, + Guid? favoriteUserId = null, BlogPostStatus? statusFilter = null, CancellationToken cancellationToken = default); @@ -22,6 +23,7 @@ public interface IBlogPostRepository : IBasicRepository Guid? blogId = null, Guid? authorId = null, Guid? tagId = null, + Guid? favoriteUserId = null, BlogPostStatus? statusFilter = null, int maxResultCount = int.MaxValue, int skipCount = 0, @@ -44,4 +46,6 @@ public interface IBlogPostRepository : IBasicRepository Task GetAuthorHasBlogPostAsync(Guid id, CancellationToken cancellationToken = default); Task HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default); + + Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogRepository.cs index 60005db319..aebdfc3f24 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogRepository.cs @@ -16,6 +16,13 @@ public interface IBlogRepository : IBasicRepository CancellationToken cancellationToken = default ); + Task> GetListWithBlogPostCountAsync( + string filter = null, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + CancellationToken cancellationToken = default); + Task GetCountAsync( string filter = null, CancellationToken cancellationToken = default diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs index e3824063e6..10ffbf964b 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs @@ -12,6 +12,7 @@ using Volo.CmsKit.Blogs; using Volo.CmsKit.Comments; using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.Localization; +using Volo.CmsKit.MarkedItems; using Volo.CmsKit.MediaDescriptors; using Volo.CmsKit.Menus; using Volo.CmsKit.Pages; @@ -91,6 +92,24 @@ public class CmsKitDomainModule : AbpModule { // TODO: Configure TagEntityTypes here... } + + if (GlobalFeatureManager.Instance.IsEnabled()) + { + Configure(options => + { + if (GlobalFeatureManager.Instance.IsEnabled()) + { + options.EntityTypes.Add( + new MarkedItemEntityTypeDefinition( + BlogPostConsts.EntityType, + StandardMarkedItems.Favorite + ) + ); + } + + // TODO: Add more entities that can be marked. + }); + } } public override void PostConfigureServices(ServiceConfigurationContext context) diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs index 2bba78505e..c8c267383e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs @@ -49,4 +49,10 @@ public interface ICommentRepository : IBasicRepository ); Task ExistsAsync(string idempotencyToken, CancellationToken cancellationToken = default); + + Task DeleteByEntityTypeAndIdAsync( + [NotNull] string entityType, + [NotNull] string entityId, + CancellationToken cancellationToken = default + ); } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/MarkedItems/MarkedItemManager.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/MarkedItems/MarkedItemManager.cs index 15c858f5c4..bb9f884dba 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/MarkedItems/MarkedItemManager.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/MarkedItems/MarkedItemManager.cs @@ -2,6 +2,8 @@ using JetBrains.Annotations; using System.Threading.Tasks; using Volo.Abp; +using System.Collections.Generic; +using System.Threading; namespace Volo.CmsKit.MarkedItems; public class MarkedItemManager : CmsKitDomainServiceBase @@ -20,7 +22,7 @@ public class MarkedItemManager : CmsKitDomainServiceBase public virtual async Task ToggleUserMarkedItemAsync( Guid creatorId, - [NotNull] string entityType, + [NotNull] string entityType, [NotNull] string entityId) { Check.NotNullOrWhiteSpace(entityType, nameof(entityType)); @@ -49,4 +51,12 @@ public class MarkedItemManager : CmsKitDomainServiceBase ); return true; } + public async Task> GetEntityIdsFilteredByUserAsync( + [NotNull] Guid userId, + [NotNull] string entityType, + [CanBeNull] Guid? tenantId = null, + CancellationToken cancellationToken = default) + { + return await UserMarkedItemRepository.GetEntityIdsFilteredByUserAsync(userId, entityType, tenantId, cancellationToken); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs index 56cef52826..418bb19408 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs @@ -40,6 +40,8 @@ public class MenuItem : AuditedAggregateRoot, IMultiTenant public Guid? PageId { get; protected set; } public Guid? TenantId { get; protected set; } + + public string RequiredPermissionName { get; set; } protected MenuItem() { @@ -55,7 +57,8 @@ public class MenuItem : AuditedAggregateRoot, IMultiTenant [CanBeNull] string target = null, [CanBeNull] string elementId = null, [CanBeNull] string cssClass = null, - [CanBeNull] Guid? tenantId = null) + [CanBeNull] Guid? tenantId = null, + [CanBeNull] string requiredPermissionName = null) : base(id) { SetDisplayName(displayName); @@ -68,6 +71,7 @@ public class MenuItem : AuditedAggregateRoot, IMultiTenant ElementId = elementId; CssClass = cssClass; TenantId = tenantId; + RequiredPermissionName = requiredPermissionName; } public void SetDisplayName([NotNull] string displayName) diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs index 266a04a910..a93683ff46 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs @@ -11,6 +11,7 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; using Volo.CmsKit.EntityFrameworkCore; +using Volo.CmsKit.MarkedItems; using Volo.CmsKit.Tags; using Volo.CmsKit.Users; @@ -18,12 +19,15 @@ namespace Volo.CmsKit.Blogs; public class EfCoreBlogPostRepository : EfCoreRepository, IBlogPostRepository { + private readonly MarkedItemManager _markedItemManager; private EntityTagManager _entityTagManager; public EfCoreBlogPostRepository( IDbContextProvider dbContextProvider, + MarkedItemManager markedItemManager, EntityTagManager entityTagManager) : base(dbContextProvider) { + _markedItemManager = markedItemManager; _entityTagManager = entityTagManager; } @@ -50,17 +54,21 @@ public class EfCoreBlogPostRepository : EfCoreRepository entityIdFilters = null; - if (tagId.HasValue) - { - entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); - } + var tagFilteredEntityIds = tagId.HasValue + ? await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken) + : null; + + var favoriteUserFilteredEntityIds = favoriteUserId.HasValue + ? await _markedItemManager.GetEntityIdsFilteredByUserAsync(favoriteUserId.Value, BlogPostConsts.EntityType) + : null; var queryable = (await GetDbSetAsync()) - .WhereIf(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) + .WhereIf(tagFilteredEntityIds != null, x => tagFilteredEntityIds.Contains(x.Id.ToString())) + .WhereIf(favoriteUserFilteredEntityIds != null, x => favoriteUserFilteredEntityIds.Contains(x.Id.ToString())) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) .WhereIf(statusFilter.HasValue, x => x.Status == statusFilter) @@ -75,6 +83,7 @@ public class EfCoreBlogPostRepository : EfCoreRepository(); var usersDbSet = dbContext.Set(); - List entityIdFilters = null; - if (tagId.HasValue) - { - entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); - } + var tagFilteredEntityIds = tagId.HasValue + ? await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken) + : null; + + var favoriteUserFilteredEntityIds = favoriteUserId.HasValue + ? await _markedItemManager.GetEntityIdsFilteredByUserAsync(favoriteUserId.Value, BlogPostConsts.EntityType) + : null; var queryable = (await GetDbSetAsync()) - .WhereIf(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) + .WhereIf(tagFilteredEntityIds != null, x => tagFilteredEntityIds.Contains(x.Id.ToString())) + .WhereIf(favoriteUserFilteredEntityIds != null, x => favoriteUserFilteredEntityIds.Contains(x.Id.ToString())) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) @@ -160,4 +172,17 @@ public class EfCoreBlogPostRepository : EfCoreRepository x.Status == BlogPostStatus.WaitingForReview, GetCancellationToken(cancellationToken)); } + + public async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId = null, CancellationToken cancellationToken = default) + { + if (targetBlogId != null) + { + await (await GetDbSetAsync()).Where(x => x.BlogId == sourceBlogId).ExecuteUpdateAsync(x => x.SetProperty(b => b.BlogId, targetBlogId.Value), GetCancellationToken(cancellationToken)); + + } + else + { + await (await GetDbSetAsync()).Where(x => x.BlogId == sourceBlogId).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); + } + } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogRepository.cs index daf915f675..5234577f02 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogRepository.cs @@ -43,6 +43,31 @@ public class EfCoreBlogRepository : EfCoreRepository> GetListWithBlogPostCountAsync( + string filter = null, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + var blogs = await (await GetListQueryAsync(filter)).OrderBy(sorting.IsNullOrEmpty() ? "creationTime desc" : sorting) + .PageBy(skipCount, maxResultCount).ToListAsync(GetCancellationToken(cancellationToken)); + + var blogIds = blogs.Select(x => x.Id).ToArray(); + + var blogPostCount = await (await GetDbContextAsync()).Set() + .Where(blogPost => blogIds.Contains(blogPost.BlogId)) + .GroupBy(blogPost => blogPost.BlogId) + .Select(x => new + { + BlogId = x.Key, + Count = x.Count() + }) + .ToListAsync(GetCancellationToken(cancellationToken)); + + return blogs.Select(blog => new BlogWithBlogPostCount(blog, blogPostCount.FirstOrDefault(x => x.BlogId == blog.Id)?.Count ?? 0)).ToList(); + } + public virtual async Task GetCountAsync(string filter = null, CancellationToken cancellationToken = default) { var query = await GetListQueryAsync(filter); diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs index 542188baf8..5dff9d72d6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs @@ -186,4 +186,9 @@ public class EfCoreCommentRepository : EfCoreRepository c.Comment.IsApproved == false) .WhereIf(CommentApproveState.Waiting == commentApproveState, c => c.Comment.IsApproved == null); } + + public async Task DeleteByEntityTypeAndIdAsync(string entityType, string entityId, CancellationToken cancellationToken = default) + { + await (await GetDbSetAsync()).Where(x => x.EntityType == entityType && x.EntityId == entityId).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs index b453f52fc5..c5b93022c6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs @@ -261,6 +261,8 @@ public static class CmsKitDbContextModelCreatingExtensions b.Property(x => x.DisplayName).IsRequired().HasMaxLength(MenuItemConsts.MaxDisplayNameLength); b.Property(x => x.Url).IsRequired().HasMaxLength(MenuItemConsts.MaxUrlLength); + + b.Property(x => x.RequiredPermissionName).HasMaxLength(MenuItemConsts.MaxRequiredPermissionNameLength); }); } else diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs index d3ae31926b..0212e5975c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs @@ -12,6 +12,7 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; using Volo.CmsKit.Blogs; +using Volo.CmsKit.MarkedItems; using Volo.CmsKit.Tags; using Volo.CmsKit.Users; @@ -19,10 +20,15 @@ namespace Volo.CmsKit.MongoDB.Blogs; public class MongoBlogPostRepository : MongoDbRepository, IBlogPostRepository { + private readonly MarkedItemManager _markedItemManager; private EntityTagManager _entityTagManager; - public MongoBlogPostRepository(IMongoDbContextProvider dbContextProvider, EntityTagManager entityTagManager) : base( + public MongoBlogPostRepository( + IMongoDbContextProvider dbContextProvider, + MarkedItemManager markedItemManager, + EntityTagManager entityTagManager) : base( dbContextProvider) { + _markedItemManager = markedItemManager; _entityTagManager = entityTagManager; } @@ -48,15 +54,19 @@ public class MongoBlogPostRepository : MongoDbRepository>(entityIdFilters.Any(), x => entityIdFilters.Contains(x.Id)) + .WhereIf>(tagFilteredEntityIds.Any(), x => tagFilteredEntityIds.Contains(x.Id)) + .WhereIf>(favoriteUserFilteredEntityIds.Any(), x => favoriteUserFilteredEntityIds.Contains(x.Id)) .WhereIf>(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf>(blogId.HasValue, x => x.BlogId == blogId) .WhereIf>(authorId.HasValue, x => x.AuthorId == authorId) @@ -69,6 +79,7 @@ public class MongoBlogPostRepository : MongoDbRepository().AsQueryable(); var queryable = blogPostQueryable - .WhereIf(entityIdFilters.Any(), x => entityIdFilters.Contains(x.Id)) + .WhereIf(tagFilteredEntityIds.Any(), x => tagFilteredEntityIds.Contains(x.Id)) + .WhereIf(favoriteUserFilteredEntityIds.Any(), x => favoriteUserFilteredEntityIds.Contains(x.Id)) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) @@ -132,6 +146,28 @@ public class MongoBlogPostRepository : MongoDbRepository> GetFavoriteEntityIdsByUserId(Guid? userId, CancellationToken cancellationToken) + { + var entityIdFilters = new List(); + if (!userId.HasValue) + { + return entityIdFilters; + } + + var entityIds = + await _markedItemManager.GetEntityIdsFilteredByUserAsync(userId.Value, BlogPostConsts.EntityType, CurrentTenant.Id, cancellationToken); + + foreach (var entityId in entityIds) + { + if (Guid.TryParse(entityId, out var parsedEntityId)) + { + entityIdFilters.Add(parsedEntityId); + } + } + + return entityIdFilters; + } + public virtual async Task SlugExistsAsync(Guid blogId, [NotNull] string slug, CancellationToken cancellationToken = default) { @@ -192,4 +228,24 @@ public class MongoBlogPostRepository : MongoDbRepository x.Status == BlogPostStatus.WaitingForReview, cancellationToken); } + + public async Task UpdateBlogAsync(Guid sourceBlogId, Guid? targetBlogId, CancellationToken cancellationToken = default) + { + cancellationToken = GetCancellationToken(cancellationToken); + var blogPosts = await (await GetMongoQueryableAsync(cancellationToken)).Where(x => x.BlogId == sourceBlogId).ToListAsync(cancellationToken); + if (targetBlogId.HasValue) + { + foreach (var blogPost in blogPosts) + { + blogPost.SetBlogId(targetBlogId.Value); + } + + await UpdateManyAsync(blogPosts, false, cancellationToken); + } + else + { + + await DeleteManyAsync(blogPosts, false, cancellationToken); + } + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogRepository.cs index b34c452621..98567899e5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogRepository.cs @@ -49,6 +49,33 @@ public class MongoBlogRepository : MongoDbRepository> GetListWithBlogPostCountAsync( + string filter = null, + string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + CancellationToken cancellationToken = default) + { + var token = GetCancellationToken(cancellationToken); + + var blogs = await GetListQueryAsync(filter, token); + + var blogIds = blogs.OrderBy(sorting.IsNullOrEmpty() ? "creationTime desc" : sorting) + .PageBy(skipCount, maxResultCount).Select(x => x.Id).ToList(); + + var blogPostCount = await (await GetMongoQueryableAsync(token)) + .Where(blogPost => blogIds.Contains(blogPost.Id)) + .GroupBy(blogPost => blogPost.BlogId) + .Select(x => new + { + BlogId = x.Key, + Count = x.Count() + }) + .ToListAsync(GetCancellationToken(cancellationToken)); + + return blogs.Select(blog => new BlogWithBlogPostCount(blog, blogPostCount.FirstOrDefault(x => x.BlogId == blog.Id) != null ? blogPostCount.FirstOrDefault(x => x.BlogId == blog.Id).Count : 0)).ToList(); + } + public virtual async Task GetCountAsync(string filter = null, CancellationToken cancellationToken = default) { var token = GetCancellationToken(cancellationToken); diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs index ec82f052e4..fab729fcd2 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Comments/MongoCommentRepository.cs @@ -207,4 +207,9 @@ public class MongoCommentRepository : MongoDbRepository c.IsApproved == false) .WhereIf(CommentApproveState.Waiting == commentApproveState, c => c.IsApproved == null); } + + public async Task DeleteByEntityTypeAndIdAsync(string entityType, string entityId, CancellationToken cancellationToken = default) + { + await (await GetDbContextAsync(cancellationToken)).Comments.DeleteManyAsync(x => x.EntityType == entityType && x.EntityId == entityId, GetCancellationToken(cancellationToken)); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs index 5e3ffe9311..762d8663d6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs @@ -8,4 +8,5 @@ public class BlogPostGetListInput : PagedAndSortedResultRequestDto public Guid? AuthorId { get; set; } public Guid? TagId { get; set; } + public bool? FilterOnFavorites { get; set; } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs index 4a9875990d..561b6a9bcb 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs @@ -26,15 +26,18 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub protected IBlogPostRepository BlogPostRepository { get; } protected ITagRepository TagRepository { get; } + protected BlogPostManager BlogPostManager { get; } public BlogPostPublicAppService( IBlogRepository blogRepository, IBlogPostRepository blogPostRepository, - ITagRepository tagRepository) + ITagRepository tagRepository, + BlogPostManager blogPostManager) { BlogRepository = blogRepository; BlogPostRepository = blogPostRepository; TagRepository = tagRepository; + BlogPostManager = blogPostManager; } public virtual async Task GetAsync( @@ -51,7 +54,10 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub { var blog = await BlogRepository.GetBySlugAsync(blogSlug); + Guid? favoriteUserId = await GetFavoriteUserIdAsync(input.FilterOnFavorites); + var blogPosts = await BlogPostRepository.GetListAsync(null, blog.Id, input.AuthorId, input.TagId, + favoriteUserId, BlogPostStatus.Published, input.MaxResultCount, input.SkipCount, input.Sorting); @@ -88,7 +94,7 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub throw new AbpAuthorizationException(); } - await BlogPostRepository.DeleteAsync(id); + await BlogPostManager.DeleteAsync(id); } public async Task GetTagNameAsync([NotNull] Guid tagId) @@ -97,4 +103,14 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub return tag.Name; } + + protected virtual async Task GetFavoriteUserIdAsync(bool? filterOnFavorites) + { + if (!filterOnFavorites.GetValueOrDefault() || !CurrentUser.IsAuthenticated) + { + return null; + } + + return CurrentUser.GetId(); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Menus/CmsKitPublicMenuContributor.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Menus/CmsKitPublicMenuContributor.cs index 7d9bc797b9..d7b879227a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Menus/CmsKitPublicMenuContributor.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Menus/CmsKitPublicMenuContributor.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; using Volo.Abp.UI.Navigation; @@ -55,7 +57,7 @@ public class CmsKitPublicMenuContributor : IMenuContributor private ApplicationMenuItem CreateApplicationMenuItem(MenuItemDto menuItem) { - return new ApplicationMenuItem( + var menu = new ApplicationMenuItem( menuItem.DisplayName, menuItem.DisplayName, menuItem.Url, @@ -65,5 +67,11 @@ public class CmsKitPublicMenuContributor : IMenuContributor menuItem.ElementId, menuItem.CssClass ); + if (!menuItem.RequiredPermissionName.IsNullOrWhiteSpace()) + { + menu.RequirePermissions(menuItem.RequiredPermissionName); + } + + return menu; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index 6326b91cb5..7033978f48 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -105,11 +105,11 @@ @if (authorId == CurrentUser.Id) { - + @L["Edit"] - + @L["Delete"] } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/MarkedItemToggle/default.css b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/MarkedItemToggle/default.css index af5f994177..02e8fccc9e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/MarkedItemToggle/default.css +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/MarkedItemToggle/default.css @@ -2,6 +2,6 @@ } .cms-markedItem-area i { - font-size: 1.5em; + font-size: 1.05rem; cursor: pointer; } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Modals/Login/LoginModal.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Modals/Login/LoginModal.cshtml index 1472d4b227..d8ca6083a6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Modals/Login/LoginModal.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Modals/Login/LoginModal.cshtml @@ -5,7 +5,8 @@ @model Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Modals.Login.LoginModalModel @{ Layout = null; - var message = String.IsNullOrEmpty(Model.ViewModel.Message) ? L["CmsKit:Modals:LoginModalDefaultMessage"] : Model.ViewModel.Message; + var message = String.IsNullOrEmpty(Model.ViewModel.Message) ? L["CmsKit:Modals:LoginModalDefaultMessage"] : + Model.ViewModel.Message; } @@ -15,8 +16,9 @@

    @message

    -
    - @L["CmsKit:Modals:Login"] + + @L["CmsKit:Modals:Login"] +
    \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml index 6074ed573f..23f214f1e5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml @@ -1,6 +1,10 @@ @page +@using Volo.CmsKit.Blogs @using Volo.CmsKit.Public.Web.Pages +@using Volo.Abp.GlobalFeatures +@using Volo.CmsKit.GlobalFeatures +@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.MarkedItemToggle @using Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs @using Volo.CmsKit.Web @@ -8,6 +12,11 @@ @model IndexModel +@{ + const string dummyImageSource = "/cms-kit/dummy-placeholder-320x180.png"; + var isMarkedItemFeatureEnabled = GlobalFeatureManager.Instance.IsEnabled() && Model.MarkedItemsFeature?.IsEnabled == true; +} + @section styles { } @@ -15,13 +24,13 @@ @section scripts { + @if (isMarkedItemFeatureEnabled) + { + + } } -@{ - const string dummyImageSource = "/cms-kit/dummy-placeholder-320x180.png"; -} - @if (Model.AuthorId.HasValue) { @@ -61,11 +70,37 @@ @if (Model.Blogs.TotalCount > 0) { + @if (isMarkedItemFeatureEnabled) + { + var filterOnFavorties = Model.FilterOnFavorites.GetValueOrDefault(); + string icon = filterOnFavorties ? "heart" : "heart-o"; + + + + } @foreach (var blog in Model.Blogs.Items) { + @if (isMarkedItemFeatureEnabled) + { +
    + + @await Component.InvokeAsync(typeof(MarkedItemToggleViewComponent), new + { + entityId = blog.Id.ToString(), + entityType = BlogPostConsts.EntityType + }) + +
    + } @if (blog.CoverImageMediaId != null) { diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs index e47dd40816..ada15afe51 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs @@ -1,9 +1,13 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Application.Dtos; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination; +using Volo.Abp.GlobalFeatures; +using Volo.CmsKit.Blogs; using Volo.CmsKit.Contents; +using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.Public.Blogs; using Volo.CmsKit.Users; @@ -22,6 +26,9 @@ public class IndexModel : CmsKitPublicPageModelBase [BindProperty(SupportsGet = true)] public Guid? AuthorId { get; set; } + [BindProperty(SupportsGet = true)] + public bool? FilterOnFavorites { get; set; } + [BindProperty(SupportsGet = true)] public Guid? TagId { get; set; } @@ -32,12 +39,17 @@ public class IndexModel : CmsKitPublicPageModelBase public CmsUserDto SelectedAuthor { get; protected set; } public string FilteredTagName { get; protected set; } + public BlogFeatureDto MarkedItemsFeature { get; private set; } protected IBlogPostPublicAppService BlogPostPublicAppService { get; } + public IBlogFeatureAppService BlogFeatureAppService { get; } - public IndexModel(IBlogPostPublicAppService blogPostPublicAppService) + public IndexModel( + IBlogPostPublicAppService blogPostPublicAppService, + IBlogFeatureAppService blogFeatureAppService) { BlogPostPublicAppService = blogPostPublicAppService; + BlogFeatureAppService = blogFeatureAppService; } public virtual async Task OnGetAsync() @@ -49,7 +61,8 @@ public class IndexModel : CmsKitPublicPageModelBase SkipCount = PageSize * (CurrentPage - 1), MaxResultCount = PageSize, AuthorId = AuthorId, - TagId = TagId + TagId = TagId, + FilterOnFavorites = FilterOnFavorites }); if (AuthorId != null) @@ -62,6 +75,13 @@ public class IndexModel : CmsKitPublicPageModelBase FilteredTagName = await BlogPostPublicAppService.GetTagNameAsync(TagId.Value); } + if (GlobalFeatureManager.Instance.IsEnabled() && + Blogs.Items.Any()) + { + var blogId = Blogs.Items.First().BlogId; + MarkedItemsFeature = await BlogFeatureAppService.GetOrDefaultAsync(blogId, GlobalFeatures.MarkedItemsFeature.Name); + } + return Page(); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/filter-on-favorites.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/filter-on-favorites.js new file mode 100644 index 0000000000..0fb87bc0ef --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/filter-on-favorites.js @@ -0,0 +1,27 @@ +$(function () { + var l = abp.localization.getResource('CmsKit'); + + let filterButton = $('.favorite-button'); + let filterOnFavorites = filterButton.attr('filter-on-favorites'); + const loginModal = new abp.ModalManager(abp.appPath + 'CmsKit/Shared/Modals/Login/LoginModal'); + + $('.favorite-button').on('click', function () { + if (!abp.currentUser.isAuthenticated) { + const currentPageRoute = window.location.pathname; + loginModal.open({ message: l("FavoritesFilterMessage"), returnUrl: currentPageRoute }); + return; + } + + let currentUrl = new URL(window.location.href); + let searchParams = currentUrl.searchParams; + + // Toggle the 'filterOnFavorites' parameter + if (filterOnFavorites) { + searchParams.delete('filterOnFavorites'); + } else { + searchParams.set('filterOnFavorites', 'true'); + } + + window.location.href = currentUrl.pathname + '?' + searchParams.toString(); + }); +}); \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/index.css b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/index.css index e9a4f2fa19..83b1e8482c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/index.css +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/index.css @@ -17,4 +17,9 @@ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; +} + +.favorite-toggle { + width: 40px; + height: 40px; } \ No newline at end of file diff --git a/modules/docs/.abpstudio/state.json b/modules/docs/.abpstudio/state.json new file mode 100644 index 0000000000..fefde3c294 --- /dev/null +++ b/modules/docs/.abpstudio/state.json @@ -0,0 +1,16 @@ +{ + "selectedKubernetesProfile": null, + "solutionRunner": { + "selectedProfile": "Default", + "targetFrameworks": [], + "applicationsStartingWithoutBuild": [], + "applicationBatchStartStates": [ + { + "profile": "Default", + "applicationOrFolder": "VoloDocs.Web", + "value": 0 + } + ], + "folderBatchStartStates": [] + } +} \ No newline at end of file diff --git a/modules/docs/Default.abprun.json b/modules/docs/Default.abprun.json new file mode 100644 index 0000000000..f9b4dd4507 --- /dev/null +++ b/modules/docs/Default.abprun.json @@ -0,0 +1,11 @@ +{ + "metadata": {}, + "applications": { + "VoloDocs.Web": { + "type": "dotnet-project", + "path": "app/VoloDocs.Web/VoloDocs.Web.csproj", + "launchUrl": "https://localhost:5001", + "kubernetesService": null + } + } +} \ No newline at end of file diff --git a/modules/docs/Volo.Docs.abpsln b/modules/docs/Volo.Docs.abpsln index 688fe0befa..44288f9b36 100644 --- a/modules/docs/Volo.Docs.abpsln +++ b/modules/docs/Volo.Docs.abpsln @@ -3,5 +3,11 @@ "Volo.Docs": { "path": "Volo.Docs.abpmdl" } + }, + "id": "c1d848e0-0b53-461b-824a-8533ba1fd82b", + "runProfiles": { + "Default": { + "path": "Default.abprun.json" + } } } \ No newline at end of file diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.Designer.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.Designer.cs similarity index 98% rename from modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.Designer.cs rename to modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.Designer.cs index a270fbcb4e..ad7dfa9576 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.Designer.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.Designer.cs @@ -13,7 +13,7 @@ using VoloDocs.EntityFrameworkCore; namespace Migrations { [DbContext(typeof(VoloDocsDbContext))] - [Migration("20240707104226_Initial")] + [Migration("20241231072012_Initial")] partial class Initial { /// @@ -22,7 +22,7 @@ namespace Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -40,6 +40,10 @@ namespace Migrations .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -116,6 +120,10 @@ namespace Migrations .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + b.Property("EntityVersion") .HasColumnType("int"); @@ -281,9 +289,13 @@ namespace Migrations .HasMaxLength(64) .HasColumnType("nvarchar(64)"); + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + b.Property("IpAddresses") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); b.Property("LastAccessed") .HasColumnType("datetime2"); diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.cs similarity index 99% rename from modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.cs rename to modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.cs index 12da8d1bca..25791fabf4 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20240707104226_Initial.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20241231072012_Initial.cs @@ -23,6 +23,7 @@ namespace Migrations RegexDescription = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), Description = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), ValueType = table.Column(type: "int", nullable: false), + CreationTime = table.Column(type: "datetime2", nullable: false), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, @@ -137,6 +138,7 @@ namespace Migrations IsStatic = table.Column(type: "bit", nullable: false), IsPublic = table.Column(type: "bit", nullable: false), EntityVersion = table.Column(type: "int", nullable: false), + CreationTime = table.Column(type: "datetime2", nullable: false), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, @@ -181,9 +183,10 @@ namespace Migrations TenantId = table.Column(type: "uniqueidentifier", nullable: true), UserId = table.Column(type: "uniqueidentifier", nullable: false), ClientId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), - IpAddresses = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + IpAddresses = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), SignedIn = table.Column(type: "datetime2", nullable: false), - LastAccessed = table.Column(type: "datetime2", nullable: true) + LastAccessed = table.Column(type: "datetime2", nullable: true), + ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs index 37601938e1..17e18940bb 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -37,6 +37,10 @@ namespace Migrations .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -113,6 +117,10 @@ namespace Migrations .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + b.Property("EntityVersion") .HasColumnType("int"); @@ -278,9 +286,13 @@ namespace Migrations .HasMaxLength(64) .HasColumnType("nvarchar(64)"); + b.Property("ExtraProperties") + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + b.Property("IpAddresses") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); b.Property("LastAccessed") .HasColumnType("datetime2"); diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Documents/IDocumentAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Documents/IDocumentAdminAppService.cs index 12bdbc87be..8931cfa5fd 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Documents/IDocumentAdminAppService.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Documents/IDocumentAdminAppService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; +using Volo.Docs.Admin.Projects; namespace Volo.Docs.Admin.Documents { @@ -21,5 +22,7 @@ namespace Volo.Docs.Admin.Documents Task ReindexAsync(Guid documentId); Task> GetFilterItemsAsync(); + + Task> GetProjectsAsync(); } } diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs index 8e70be19e4..a6359c2c9b 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs @@ -8,6 +8,7 @@ using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Caching; +using Volo.Docs.Admin.Projects; using Volo.Docs.Caching; using Volo.Docs.Documents; using Volo.Docs.Documents.FullSearch.Elastic; @@ -220,6 +221,12 @@ namespace Volo.Docs.Admin.Documents return ObjectMapper.Map, List>(documents); } + public virtual async Task> GetProjectsAsync() + { + var projects = await _projectRepository.GetListWithoutDetailsAsync(); + return ObjectMapper.Map, List>(projects); + } + private async Task UpdateDocumentUpdateInfoCache(Document document) { diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/DocumentsAdminClientProxy.Generated.cs b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/DocumentsAdminClientProxy.Generated.cs index 8a6e2b7173..b113d74451 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/DocumentsAdminClientProxy.Generated.cs +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/DocumentsAdminClientProxy.Generated.cs @@ -9,6 +9,7 @@ using Volo.Abp.Http.Client; using Volo.Abp.Http.Client.ClientProxying; using Volo.Abp.Http.Modeling; using Volo.Docs.Admin.Documents; +using Volo.Docs.Admin.Projects; // ReSharper disable once CheckNamespace namespace Volo.Docs.Admin; @@ -69,4 +70,9 @@ public partial class DocumentsAdminClientProxy : ClientProxyBase>(nameof(GetFilterItemsAsync)); } + + public virtual async Task> GetProjectsAsync() + { + return await RequestAsync>(nameof(GetProjectsAsync)); + } } diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/docs-admin-generate-proxy.json similarity index 97% rename from modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json rename to modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/docs-admin-generate-proxy.json index 7875b68b43..e433e06ef9 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/docs-admin-generate-proxy.json +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/ClientProxies/Volo/Docs/Admin/docs-admin-generate-proxy.json @@ -125,6 +125,14 @@ "type": "System.Collections.Generic.List", "typeSimple": "[Volo.Docs.Admin.Documents.DocumentInfoDto]" } + }, + { + "name": "GetProjectsAsync", + "parametersOnMethod": [], + "returnValue": { + "type": "System.Collections.Generic.List", + "typeSimple": "[Volo.Docs.Admin.Projects.ProjectWithoutDetailsDto]" + } } ] } @@ -558,6 +566,21 @@ }, "allowAnonymous": null, "implementFrom": "Volo.Docs.Admin.Documents.IDocumentAdminAppService" + }, + "GetProjectsAsync": { + "uniqueName": "GetProjectsAsync", + "name": "GetProjectsAsync", + "httpMethod": "GET", + "url": "api/docs/admin/documents/GetProjects", + "supportedVersions": [], + "parametersOnMethod": [], + "parameters": [], + "returnValue": { + "type": "System.Collections.Generic.List", + "typeSimple": "[Volo.Docs.Admin.Projects.ProjectWithoutDetailsDto]" + }, + "allowAnonymous": null, + "implementFrom": "Volo.Docs.Admin.Documents.IDocumentAdminAppService" } } }, diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/Volo.Docs.Admin.HttpApi.Client.abppkg b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/Volo.Docs.Admin.HttpApi.Client.abppkg index 7deef5e383..c78fc5b9ee 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/Volo.Docs.Admin.HttpApi.Client.abppkg +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi.Client/Volo.Docs.Admin.HttpApi.Client.abppkg @@ -1,3 +1,15 @@ { - "role": "lib.http-api-client" + "role": "lib.http-api-client", + "proxies": { + "csharp": { + "VoloDocs.Web-docs-admin": { + "applicationName": "VoloDocs.Web", + "module": "docs-admin", + "url": "https://localhost:5001", + "folder": "ClientProxies\\Volo\\Docs\\Admin", + "serviceType": "application", + "withoutContracts": true + } + } + } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocumentsAdminController.cs b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocumentsAdminController.cs index eb78095bf2..ddf303ffaf 100644 --- a/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocumentsAdminController.cs +++ b/modules/docs/src/Volo.Docs.Admin.HttpApi/Volo/Docs/Admin/DocumentsAdminController.cs @@ -7,6 +7,7 @@ using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.AspNetCore.Mvc; using Volo.Docs.Admin.Documents; +using Volo.Docs.Admin.Projects; namespace Volo.Docs.Admin { @@ -71,5 +72,12 @@ namespace Volo.Docs.Admin { return await _documentAdminAppService.GetFilterItemsAsync(); } + + [HttpGet] + [Route("GetProjects")] + public virtual Task> GetProjectsAsync() + { + return _documentAdminAppService.GetProjectsAsync(); + } } } diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Documents/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Documents/Index.cshtml.cs index 194ad6780f..53d78baf39 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Documents/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Documents/Index.cshtml.cs @@ -2,23 +2,24 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Volo.Docs.Admin.Documents; using Volo.Docs.Admin.Projects; namespace Volo.Docs.Admin.Pages.Docs.Admin.Documents; -[Authorize(DocsAdminPermissions.Projects.Default)] +[Authorize(DocsAdminPermissions.Documents.Default)] public class IndexModel : DocsAdminPageModel { - private readonly IProjectAdminAppService _projectAdminAppService; + private readonly IDocumentAdminAppService _documentAdminAppService; public List Projects { get; set; } - public IndexModel(IProjectAdminAppService projectAdminAppService) + public IndexModel(IDocumentAdminAppService documentAdminAppService) { - _projectAdminAppService = projectAdminAppService; + _documentAdminAppService = documentAdminAppService; } public virtual async Task OnGet() { - Projects = await _projectAdminAppService.GetListWithoutDetailsAsync(); + Projects = await _documentAdminAppService.GetProjectsAsync(); return Page(); } } diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.abppkg b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.abppkg index 930c4018b3..4e12d28da1 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.abppkg +++ b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.abppkg @@ -1,3 +1,14 @@ { - "role": "lib.mvc" + "role": "lib.mvc", + "proxies": { + "Javascript": { + "VoloDocs.Web-docs-admin": { + "applicationName": "VoloDocs.Web", + "module": "docs-admin", + "url": "https://localhost:5001", + "output": "wwwroot/client-proxies", + "serviceType": "application" + } + } + } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js b/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js index 303ca5fdb0..3a1c6930cf 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js +++ b/modules/docs/src/Volo.Docs.Admin.Web/wwwroot/client-proxies/docs-admin-proxy.js @@ -68,6 +68,13 @@ }, ajaxParams)); }; + volo.docs.admin.documentsAdmin.getProjects = function(ajaxParams) { + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/docs/admin/documents/GetProjects', + type: 'GET' + }, ajaxParams)); + }; + })(); // controller volo.docs.admin.projectsAdmin diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs index 2f1afce342..f19b9f41df 100644 --- a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs @@ -15,6 +15,12 @@ namespace Volo.Docs.Documents [JsonPropertyName("items")] public List Items { get; set; } + + [JsonPropertyName("isLazyExpandable")] + public bool IsLazyExpandable { get; set; } + + [JsonPropertyName("isIndex")] + public bool IsIndex { get; set; } public bool IsLeaf => !HasChildItems; diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs b/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs new file mode 100644 index 0000000000..cac52e8d5f --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Docs.Areas.Models.DocumentNavigation; +using Volo.Docs.Documents; +using Volo.Docs.Utils; + +namespace Volo.Docs.Areas.Documents; + +[RemoteService(Name = DocsRemoteServiceConsts.RemoteServiceName)] +[Area(DocsRemoteServiceConsts.ModuleName)] +[ControllerName("DocumentNavigation")] +[Route("/docs/document-navigation")] +public class DocumentNavigationController : AbpController +{ + private readonly IDocumentAppService _documentAppService; + private readonly IDocsLinkGenerator _docsLinkGenerator; + + public DocumentNavigationController(IDocumentAppService documentAppService, IDocsLinkGenerator docsLinkGenerator) + { + _documentAppService = documentAppService; + _docsLinkGenerator = docsLinkGenerator; + } + + [HttpGet] + [Route("")] + public virtual async Task GetNavigationAsync(GetNavigationNodeWithLinkModel input) + { + var navigationNode = await _documentAppService.GetNavigationAsync(new GetNavigationDocumentInput + { + LanguageCode = input.LanguageCode, + Version = input.Version, + ProjectId = input.ProjectId + }); + + NormalPath(navigationNode, input); + + return navigationNode; + } + + protected virtual void NormalPath(NavigationNode node, GetNavigationNodeWithLinkModel input) + { + if (node.HasChildItems) + { + foreach (var item in node.Items) + { + NormalPath(item, input); + } + } + + if (UrlHelper.IsExternalLink(node.Path)) + { + return; + } + + node.Path = RemoveFileExtensionFromPath(node.Path, input.ProjectFormat); + if (node.Path.IsNullOrWhiteSpace()) + { + node.Path = "javascript:;"; + return; + } + + node.Path = _docsLinkGenerator.GenerateLink(input.ProjectName, input.LanguageCode, input.RouteVersion, node.Path); + } + + private string RemoveFileExtensionFromPath(string path, string projectFormat) + { + if (path == null) + { + return null; + } + + return path.EndsWith("." + projectFormat) + ? path.Left(path.Length - projectFormat.Length - 1) + : path; + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs b/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs index 6d2cb8c9ba..e6c75d0161 100644 --- a/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs +++ b/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs @@ -72,11 +72,14 @@ namespace Volo.Docs.Areas.Documents.TagHelpers var isAnyNodeOpenedInThisLevel = IsAnyNodeOpenedInThisLevel(node); - node.Items?.ForEach(innerNode => + if (!node.IsLazyExpandable || isAnyNodeOpenedInThisLevel) { - content += GetParentNode(innerNode, isAnyNodeOpenedInThisLevel); - }); - + node.Items?.ForEach(innerNode => + { + content += GetParentNode(innerNode, isAnyNodeOpenedInThisLevel); + }); + } + var result = node.IsEmpty ? content : GetLeafNode(node, content); return result; @@ -121,6 +124,11 @@ namespace Volo.Docs.Areas.Documents.TagHelpers listItemCss += " selected-tree"; } + if (node.IsLazyExpandable) + { + listItemCss += " lazy-expand"; + } + string listInnerItem; if (node.Path.IsNullOrEmpty() && node.IsLeaf) { diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs b/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs new file mode 100644 index 0000000000..085cc2de0e --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; +using Volo.Docs.Language; +using Volo.Docs.Projects; + +namespace Volo.Docs.Areas.Models.DocumentNavigation; + +public class GetNavigationNodeWithLinkModel +{ + public Guid ProjectId { get; set; } + + [DynamicStringLength(typeof(ProjectConsts), nameof(ProjectConsts.MaxVersionNameLength))] + public string Version { get; set; } + + [Required] + [DynamicStringLength(typeof(LanguageConsts), nameof(LanguageConsts.MaxLanguageCodeLength))] + public string LanguageCode { get; set; } + + [Required] + public string ProjectName { get; set; } + + [Required] + public string ProjectFormat { get; set; } + + [Required] + public string RouteVersion { get; set; } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 2167119a0d..fb1f232cb3 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -16,6 +16,7 @@ @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Docs @using Volo.Docs.Areas.Documents.TagHelpers +@using Volo.Docs.Documents @using Volo.Docs.Localization @using Volo.Docs.Pages.Documents.Project @using Volo.Docs.Pages.Documents.Shared.ErrorComponent @@ -52,6 +53,17 @@ @if (Model.LoadSuccess) { + @@ -292,6 +304,73 @@ + @if (Model.Navigation != null && Model.Navigation.Items != null) + { + var currentNode = Model.Navigation.Items.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + var navigation = Model.Navigation.FindNavigation(Model.DocumentNameWithExtension); + var documentExtension = System.IO.Path.GetExtension(Model.DocumentNameWithExtension); + NavigationNode previousNode = null; + if (currentNode != null) + { + while (currentNode != null) + { + if (!string.IsNullOrWhiteSpace(currentNode.Path) && previousNode != currentNode) + { + previousNode = currentNode; + PrintNode(currentNode); + } + else + { + var indexDocument = FindIndexNode(currentNode); + if (indexDocument != null && indexDocument != previousNode) + { + PrintNode(indexDocument, currentNode.Text); + } + else if(currentNode != previousNode) + { + + } + + previousNode = indexDocument; + } + + currentNode = currentNode?.Items?.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + } + } + + void PrintNode(NavigationNode node, string text = null) + { + var extension = System.IO.Path.GetExtension(node.Path); + var path = string.IsNullOrWhiteSpace(extension) ? node.Path : node.Path.Substring(0, node.Path.Length - extension.Length); + + } + + NavigationNode FindIndexNode(NavigationNode node) + { + var nextNode = node.Items?.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + while (nextNode != null && string.IsNullOrEmpty(nextNode.Path)) + { + nextNode = nextNode.Items.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + } + + if(nextNode == null) + { + return node.Items?.FirstOrDefault(n => n.IsIndex); + } + + var lastSlashIndex = nextNode.Path.LastIndexOf('/'); + var path = lastSlashIndex < 0 ? nextNode.Path : nextNode.Path.Substring(0, lastSlashIndex); + return node.Items.FirstOrDefault(n => n.Path != null && (n.IsSelected(path + documentExtension) || n.IsSelected(path + "/index" + documentExtension))) ?? node.Items.FirstOrDefault(n => n.IsIndex); + } + }