diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt.json new file mode 100644 index 0000000000..dc343133aa --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt.json @@ -0,0 +1,22 @@ +{ + "culture": "pt", + "texts": { + "Account": "Conta ABP - Iniciar sessão e registar | ABP.IO", + "Welcome": "Bem-vindo", + "UseOneOfTheFollowingLinksToContinue": "Utilize uma das seguintes ligações para continuar", + "FrameworkHomePage": "Página inicial da framework", + "FrameworkDocumentation": "Documentação da framework", + "OfficialBlog": "Blogue oficial", + "CommercialHomePage": "Página inicial comercial", + "CommercialSupportWebSite": "Site de apoio comercial", + "CommunityWebSite": "Site da comunidade ABP", + "ManageAccount": "A minha conta | ABP.IO", + "ManageYourProfile": "Gerir o seu perfil", + "ReturnToApplication": "Voltar à aplicação", + "IdentityUserNotAvailable:Deleted": "Este endereço de email não está disponível. Motivo: já foi eliminado.", + "SelectYourOrganization": "Selecione a sua organização", + "PleaseSelectOrganization": "Selecione uma organização para continuar", + "Continue": "Continuar", + "CaptchaExplanation": "Calcule a expressão matemática abaixo e introduza a resposta." + } +} \ No newline at end of file diff --git a/common.props b/common.props index f395b701bf..39be4f5c66 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.7.0-rc.4 - 5.7.0-rc.4 + 10.8.0-preview + 5.8.0-preview $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2024-07-31 v8_3_Preview/post.md b/docs/en/Blog-Posts/2024-07-31 v8_3_Preview/post.md index fc3590b0d5..dff0e1fdae 100644 --- a/docs/en/Blog-Posts/2024-07-31 v8_3_Preview/post.md +++ b/docs/en/Blog-Posts/2024-07-31 v8_3_Preview/post.md @@ -165,7 +165,7 @@ There are exciting articles contributed by the ABP community as always. I will h * [Create a Generic HTTP Service to Consume a Web API](https://abp.io/community/articles/create-a-generic-http-service-to-consume-a-web-api-yidme2kq) by [Bart Van Hoey](https://github.com/bartvanhoey) * [Use User-Defined Function Mapping for Global Filter](https://abp.io/community/articles/use-userdefined-function-mapping-for-global-filter-pht26l07) by [Liming Ma](https://github.com/maliming) -* [How to use .NET Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk) by [Berkan Şaşmaz](https://twitter.com/berkansasmazz) +* [How to use Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk) by [Berkan Şaşmaz](https://twitter.com/berkansasmazz) * [Exciting New Feature in ABP.IO CMS Kit: Marked Item System](https://abp.io/community/articles/exciting-new-feature-in-abp.io-cms-kit-marked-item-system.-2hvpq0me) by [Suhaib Mousa](https://abp.io/community/members/suhaibmousa032@gmail.com) 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/create) to the ABP Community. diff --git a/docs/en/Blog-Posts/2024-09-17-My-Impressions-at-DotNext-2024-Conference/post.md b/docs/en/Blog-Posts/2024-09-17-My-Impressions-at-DotNext-2024-Conference/post.md index c6ac51d273..c426dad0c6 100644 --- a/docs/en/Blog-Posts/2024-09-17-My-Impressions-at-DotNext-2024-Conference/post.md +++ b/docs/en/Blog-Posts/2024-09-17-My-Impressions-at-DotNext-2024-Conference/post.md @@ -14,7 +14,7 @@ Last week, I had the chance of being a speaker at **DotNext 2024** in Moscow. [J One of the standout aspects of the conference was its strong technical focus, particularly on deep-dive .NET topics. Talks covered subjects like **low-level optimizations**, architecture, performance, and platform internals. -The conference started with Sergei Benzenko's talk "What's New in .NET 9". There are important topics like ".NET Aspire in Action", "AI-Driven Software Development", "DDD and Strategic Design" and "OAuth 2.0 User-Managed Access in ASP.NET Core with Keycloak". +The conference started with Sergei Benzenko's talk "What's New in .NET 9". There are important topics like "Aspire in Action", "AI-Driven Software Development", "DDD and Strategic Design" and "OAuth 2.0 User-Managed Access in ASP.NET Core with Keycloak". ![DotNext 2024 Speakers](speakers.png) 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 index db52eed7d5..79b50c3d6b 100644 --- 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 @@ -209,7 +209,7 @@ There are exciting articles contributed by the ABP community as always. I will h * [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) +* [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/) diff --git a/docs/en/Blog-Posts/2025-06-10-Announcing-ABP-Studio-1.0-Stable-Release/POST.md b/docs/en/Blog-Posts/2025-06-10-Announcing-ABP-Studio-1.0-Stable-Release/POST.md index 8b37325e7e..38ccce7748 100644 --- a/docs/en/Blog-Posts/2025-06-10-Announcing-ABP-Studio-1.0-Stable-Release/POST.md +++ b/docs/en/Blog-Posts/2025-06-10-Announcing-ABP-Studio-1.0-Stable-Release/POST.md @@ -87,7 +87,7 @@ We will keep releasing new versions with exciting features based on our roadmap - **OpenTelemetry Integration:** We'll be integrating OpenTelemetry support directly into the startup templates, making distributed tracing and observability a seamless part of your application from day one. - **LeptonX Theme Builder**: Allowing users to determine styling, colour palette and easily override their project's theme styles. - **Monitor dashboards of the tools used in the solution (e.g. Kubernetes, Redis, Grafana, etc...)** -- **Pre-configured .NET Aspire for the Microservice Startup Template** +- **Pre-configured Aspire for the Microservice Startup Template** - **and more...** We are incredibly excited about the future of ABP Studio and can't wait to share the next set of features with you. Your comments and suggestions are invaluable to us. If you have any feedback, please drop a comment below. diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md index a56dd5a464..7ef2be8c6b 100644 --- a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md +++ b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md @@ -1,8 +1,8 @@ -# Announcing .NET Aspire Integration for ABP Microservice Template +# Announcing Aspire Integration for ABP Microservice Template -We are excited to announce the integration of **.NET Aspire** into the ABP microservice solution, available starting with **ABP Studio v2.0.0**. This integration brings a unified development experience for building, running, debugging, and deploying distributed applications. With Aspire, you can now orchestrate your entire microservice ecosystem with a single command, eliminating complex configurations and making local development effortless. +We are excited to announce the integration of **Aspire** into the ABP microservice solution, available starting with **ABP Studio v2.0.0**. This integration brings a unified development experience for building, running, debugging, and deploying distributed applications. With Aspire, you can now orchestrate your entire microservice ecosystem with a single command, eliminating complex configurations and making local development effortless. -## What is .NET Aspire? +## What is Aspire? [Aspire](https://aspire.dev/get-started/what-is-aspire/) is a cloud-ready stack designed to streamline the development of distributed applications. It provides: @@ -14,7 +14,7 @@ We are excited to announce the integration of **.NET Aspire** into the ABP micro ## How Does It Work with ABP? -When you enable .NET Aspire in an ABP microservice solution, you get a fully integrated development experience where: +When you enable Aspire in an ABP microservice solution, you get a fully integrated development experience where: - All microservices, gateways, and applications are orchestrated through a single entry point (AppHost). - Infrastructure containers (databases, Redis, RabbitMQ, Elasticsearch, etc.) are managed as code. @@ -24,8 +24,8 @@ When you enable .NET Aspire in an ABP microservice solution, you get a fully int When creating a new microservice solution via ABP Studio: -1. In the solution creation wizard, look for the **".NET Aspire Integration"** step. -2. Toggle the option to **enable .NET Aspire**. +1. In the solution creation wizard, look for the **"Aspire Integration"** step. +2. Toggle the option to **enable Aspire**. 3. Complete the wizard—Aspire projects will be generated along with your solution. ![Enable Aspire in ABP Studio](aspire-configuration.png) @@ -38,7 +38,7 @@ When Aspire is enabled, two additional projects are added to your solution: ### AppHost (Orchestrator) -[`AppHost`](https://aspire.dev/get-started/app-host/) is the .NET Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. It provides: +[`AppHost`](https://aspire.dev/get-started/app-host/) is the Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. It provides: - **Centralized orchestration**: Start your entire microservice ecosystem with a single command. - **Code-first infrastructure**: Databases, Redis, RabbitMQ, Elasticsearch, and observability tools are defined programmatically. @@ -162,9 +162,9 @@ The database management admin tool varies by database type: ## Get Started Today -Ready to experience the power of .NET Aspire with ABP? Create a new microservice solution in ABP Studio and enable the .NET Aspire integration option. For detailed documentation, visit our [.NET Aspire Integration documentation](https://abp.io/docs/latest/solution-templates/microservice/aspire-integration). +Ready to experience the power of Aspire with ABP? Create a new microservice solution in ABP Studio and enable the Aspire integration option. For detailed documentation, visit our [Aspire Integration documentation](https://abp.io/docs/latest/solution-templates/microservice/aspire-integration). -To learn more about .NET Aspire, visit: [https://aspire.dev](https://aspire.dev/get-started/what-is-aspire/) +To learn more about Aspire, visit: [https://aspire.dev](https://aspire.dev/get-started/what-is-aspire/) We are excited to bring this integration to you and can't wait to hear your feedback. If you have any questions or suggestions, please drop a comment below. diff --git a/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/POST.md b/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/POST.md index b8a4d5bf65..f065aacbf5 100644 --- a/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/POST.md +++ b/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/POST.md @@ -10,7 +10,7 @@ You can check the [Get Started page](https://abp.io/get-started) to see how to g 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](studio-switch-to-preview.png) +![studio-switch-to-preview](https://raw.githubusercontent.com/abpframework/abp/refs/heads/dev/docs/en/Blog-Posts/2026-08-05%20v10_7_Preview/studio-switch-to-preview.png) ## Migration Guide @@ -82,7 +82,7 @@ Generated React pages include list, search, sorting, paging, filtering, export, The generator respects the entity and field configuration you define in ABP Suite, including `ShowOn*`, `IsFilterable`, and `ReadonlyOnEditModal` options. Navigation lookups use server-side search. -![React CRUD page generation demo](react-crud-page.mp4) +![](https://raw.githubusercontent.com/abpframework/abp/refs/heads/dev/docs/en/Blog-Posts/2026-08-05%20v10_7_Preview/react-crud-page.gif) ### ABP Suite Decimal Precision diff --git a/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/react-crud-page.gif b/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/react-crud-page.gif new file mode 100644 index 0000000000..12779ac98b Binary files /dev/null and b/docs/en/Blog-Posts/2026-08-05 v10_7_Preview/react-crud-page.gif differ diff --git a/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md index c6a37d85cd..f9aa44aa2e 100644 --- a/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md +++ b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md @@ -7,9 +7,9 @@ Here's the summary of .NET 8 features and enhancements: ## What's new in .NET 8 -### .NET Aspire +### Aspire -[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) is a tool to observe and manage distributed web applications. It's still preview version. You can manage your containers, executables, logs, traces and metrics of your running web application. For more information see this article https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-simplifying-cloud-native-development-with-dotnet-8/ +[Aspire](https://aspire.dev/) is a tool to observe and manage distributed web applications. It's still preview version. You can manage your containers, executables, logs, traces and metrics of your running web application. For more information see this article https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-simplifying-cloud-native-development-with-dotnet-8/ ### Serialization diff --git a/docs/en/Community-Articles/2024-03-19-join-abpio-at-modern-net-web-day/post.md b/docs/en/Community-Articles/2024-03-19-join-abpio-at-modern-net-web-day/post.md index df8844f18a..d8a7dc0a9e 100644 --- a/docs/en/Community-Articles/2024-03-19-join-abpio-at-modern-net-web-day/post.md +++ b/docs/en/Community-Articles/2024-03-19-join-abpio-at-modern-net-web-day/post.md @@ -11,7 +11,7 @@ Join us dive into the magic of .NET 8 at the community live stream event, "Moder Topics that will be covered include: * *ASP.NET Core*: Crafting Maintainable Microservices -* *Cloud Native development with .NET Aspire* +* *Cloud Native development with Aspire* * *Developer Productivity with Visual Studio and .NET* * *GitHub Copilot configuration, extension, tips and tricks* * *User Experience and Front-End Development*: Delve into responsive design, accessibility, and performance optimization. Discuss modern front-end frameworks (Blazor, React, Angular, etc.) in the .NET ecosystem. Share strategies for creating delightful user interfaces. diff --git a/docs/en/Community-Articles/2024-04-16-welcome-to-abp-dotnet-conf24-a-decade-of-net-innovation/post.md b/docs/en/Community-Articles/2024-04-16-welcome-to-abp-dotnet-conf24-a-decade-of-net-innovation/post.md index 92635c6f7e..f878d9ce2d 100644 --- a/docs/en/Community-Articles/2024-04-16-welcome-to-abp-dotnet-conf24-a-decade-of-net-innovation/post.md +++ b/docs/en/Community-Articles/2024-04-16-welcome-to-abp-dotnet-conf24-a-decade-of-net-innovation/post.md @@ -50,7 +50,7 @@ As we always be careful with topics we choose on our regular **[ABP Community Ta * 🎙️[Adora Nwodo](https://abp.io/conference/2024/speakers/adora-nwodo), Designing Secure Cloud Native Apps with .NET and Azure * 🎙️[Nicola Iarocci](https://abp.io/conference/2024/speakers/nicola-iarocci), C# 12 What's new and interesting * 🎙️[Jimmy Engström](https://abp.io/conference/2024/speakers/jimmy-engstrom), Connecting gadgets to Blazor -* 🎙️[Juergen Gutsch](https://abp.io/conference/2024/speakers/juergen-gutsch), Building cloud native applications with .NET Aspire +* 🎙️[Juergen Gutsch](https://abp.io/conference/2024/speakers/juergen-gutsch), Building cloud native applications with Aspire * 🎙️[Halil Ibrahim Kalkan](https://abp.io/conference/2024/speakers/halil-ibrahim-kalkan), Designing Modular Monolith for Microservice Architecture * 🎙️[Shaun Lawrence](https://abp.io/conference/2024/speakers/shaun-lawrance), Building games in .NET MAUI * 🎙️[Jamie Taylor](https://abp.io/conference/2024/speakers/jamie-taylor), Empathy, Sympathy and Compassion diff --git a/docs/en/Community-Articles/2024-05-16-abp-dotnet-conference-2024-wrap-up/post.md b/docs/en/Community-Articles/2024-05-16-abp-dotnet-conference-2024-wrap-up/post.md index 426b01639c..1d509fadd4 100644 --- a/docs/en/Community-Articles/2024-05-16-abp-dotnet-conference-2024-wrap-up/post.md +++ b/docs/en/Community-Articles/2024-05-16-abp-dotnet-conference-2024-wrap-up/post.md @@ -33,7 +33,7 @@ The success of ABP Dotnet Conference 2024 would not have been possible without t * 🎙️[Adora Nwodo](https://abp.io/conference/2024/speakers/adora-nwodo), Designing Secure Cloud Native Apps with .NET and Azure * 🎙️[Nicola Iarocci](https://abp.io/conference/2024/speakers/nicola-iarocci), C# 12 What's new and interesting * 🎙️[Jimmy Engström](https://abp.io/conference/2024/speakers/jimmy-engstrom), Connecting gadgets to Blazor -* 🎙️[Juergen Gutsch](https://abp.io/conference/2024/speakers/juergen-gutsch), Building cloud native applications with .NET Aspire +* 🎙️[Juergen Gutsch](https://abp.io/conference/2024/speakers/juergen-gutsch), Building cloud native applications with Aspire * 🎙️[Halil Ibrahim Kalkan](https://abp.io/conference/2024/speakers/halil-ibrahim-kalkan), Designing Modular Monolith for Microservice Architecture * 🎙️[Shaun Lawrence](https://abp.io/conference/2024/speakers/shaun-lawrance), Building games in .NET MAUI * 🎙️[Jamie Taylor](https://abp.io/conference/2024/speakers/jamie-taylor), Empathy, Sympathy and Compassion 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 4afe83c7c9..c5529d2de2 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 @@ -1,17 +1,17 @@ -# How to use .NET Aspire with ABP framework +# How to use Aspire with ABP framework -[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) is an opinionated, cloud-ready stack designed for building observable, production-ready, and distributed applications. On the other hand, the [ABP framework](https://docs.abp.io/en/abp/latest) offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. This guide explores how to combine .NET Aspire with ABP, enabling developers to create observable, and feature-rich applications. +[Aspire](https://aspire.dev/get-started/what-is-aspire/) is an opinionated, cloud-ready stack designed for building observable, production-ready, and distributed applications. On the other hand, the [ABP framework](https://docs.abp.io/en/abp/latest) offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. This guide explores how to combine Aspire with ABP, enabling developers to create observable, and feature-rich applications. -## When to Use .NET Aspire? +## When to Use Aspire? -Using .NET Aspire with the ABP framework can be beneficial in various scenarios where you need to combine the strengths of both technologies. Here are some situations when using .NET Aspire with ABP can be advantageous: +Using Aspire with the ABP framework can be beneficial in various scenarios where you need to combine the strengths of both technologies. Here are some situations when using Aspire with ABP can be advantageous: -- **Enterprise Web Applications:** ABP is well-suited for building enterprise web applications with its opinionated architecture and best practices. When combined with .NET Aspire, you can leverage ABP's features for rapid development of user interfaces, backend services, and business logic while benefiting from .NET Aspire's cloud-native capabilities and observability features. -- **Observability and Monitoring:** .NET Aspire's emphasis on observability, including logging, monitoring, and tracing, can enhance ABP applications by providing deeper insights into system behavior, performance metrics, and diagnostics, which is key for maintaining and optimizing enterprise-grade applications. +- **Enterprise Web Applications:** ABP is well-suited for building enterprise web applications with its opinionated architecture and best practices. When combined with Aspire, you can leverage ABP's features for rapid development of user interfaces, backend services, and business logic while benefiting from Aspire's cloud-native capabilities and observability features. +- **Observability and Monitoring:** Aspire's emphasis on observability, including logging, monitoring, and tracing, can enhance ABP applications by providing deeper insights into system behavior, performance metrics, and diagnostics, which is key for maintaining and optimizing enterprise-grade applications. ## Creating a new ABP Solution -To demonstrate the usage of .NET Aspire with the ABP framework, I've created an ABP solution. If you want to create the same solution from scratch, follow the steps below: +To demonstrate the usage of Aspire with the ABP framework, I've created an ABP solution. If you want to create the same solution from scratch, follow the steps below: Install the ABP CLI if you haven't installed it before: @@ -27,12 +27,12 @@ abp new AspirationalAbp -u mvc --database-provider ef -dbms PostgreSQL --csf --t > The startup template selection matters for this article. I chose these options so that the demo solution can cover complex scenarios. -**Disclaimer-I:** This article is based on version `8.0.1` of .NET Aspire and version `8.2.0` of ABP Framework. +**Disclaimer-I:** This article is based on version `8.0.1` of Aspire and version `8.2.0` of ABP Framework. -**Disclaimer-II:** ABP and .NET Aspire may not be fully compatible in some respects. This article aims to explain how these two technologies can be used together in the simplest way possible, even if they are not fully compatible. -## Add .NET Aspire +**Disclaimer-II:** ABP and Aspire may not be fully compatible in some respects. This article aims to explain how these two technologies can be used together in the simplest way possible, even if they are not fully compatible. +## Add Aspire -After creating the solution, run the following commands in the `src` folder of your solution to add .NET Aspire: +After creating the solution, run the following commands in the `src` folder of your solution to add Aspire: ```bash // Adding AppHost @@ -46,11 +46,11 @@ dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.ServiceDefaults/Aspirati These commands add two new projects to the solution: - **AspirationalAbp.AppHost**: An orchestrator project designed to connect and configure the different projects and services of your app. -- **AspirationalAbp.ServiceDefaults**: A .NET Aspire shared project to manage configurations that are reused across the projects in your solution related to [resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience), [service discovery](https://learn.microsoft.com/en-us/dotnet/aspire/service-discovery/overview), and [telemetry](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/telemetry). +- **AspirationalAbp.ServiceDefaults**: An Aspire shared project to manage configurations that are reused across the projects in your solution related to [resilience](https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience), [service discovery](https://aspire.dev/fundamentals/service-discovery/), and [telemetry](https://aspire.dev/fundamentals/telemetry/). -We have added .NET Aspire to our ABP based solution, but we have not registered our projects in the .NET Aspire orchestration. Now, let's enroll our projects, which implement the db migrator, web user interface, API, and auth, in .NET Aspire orchestration. +We have added Aspire to our ABP based solution, but we have not registered our projects in the Aspire orchestration. Now, let's enroll our projects, which implement the db migrator, web user interface, API, and auth, in Aspire orchestration. -## Registering projects to .NET Aspire orchestration +## Registering projects to Aspire orchestration First of all, we need to add the reference of related projects to the `AspirationalAbp.AppHost` project. For this, add the following `ItemGroups` to the `AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj` file: @@ -130,11 +130,11 @@ With the code above, the following operations were performed below: 7. Adds the `Web` project, referencing Redis. 8. Builds and runs the application. -Now let's make the projects we added to the app host compatible with .NET Aspire. +Now let's make the projects we added to the app host compatible with Aspire. ## Configuring Projects for Aspire -To make the `AspirationalAbp.DbMigrator`, `AspirationalAbp.AuthServer`, `AspirationalAbp.HttpApi.Host`, and `AspirationalAbp.Web` projects compatible with .NET Aspire, we need to add and configure several packages. For that, we need to add the `Aspire.StackExchange.Redis` package to all these projects and the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. Additionally, we will add the `AspirationalAbp.ServiceDefaults` reference to host projects except `AspirationalAbp.DbMigrator`. Also, we need to convert [Serilog](https://serilog.net/) events into [OpenTelemetry](https://opentelemetry.io/) `LogRecord`s, for that we will add a `Serilog.Sinks.OpenTelemetry` reference to host projects. Let's begin with configuring `AspirationalAbp.DbMigrator`. +To make the `AspirationalAbp.DbMigrator`, `AspirationalAbp.AuthServer`, `AspirationalAbp.HttpApi.Host`, and `AspirationalAbp.Web` projects compatible with Aspire, we need to add and configure several packages. For that, we need to add the `Aspire.StackExchange.Redis` package to all these projects and the `Aspire.Npgsql.EntityFrameworkCore.PostgreSQL` package to the `AspirationalAbp.EntityFrameworkCore` project. Additionally, we will add the `AspirationalAbp.ServiceDefaults` reference to host projects except `AspirationalAbp.DbMigrator`. Also, we need to convert [Serilog](https://serilog.net/) events into [OpenTelemetry](https://opentelemetry.io/) `LogRecord`s, for that we will add a `Serilog.Sinks.OpenTelemetry` reference to host projects. Let's begin with configuring `AspirationalAbp.DbMigrator`. ### AspirationalAbp.DbMigrator @@ -211,7 +211,7 @@ To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelem /// .CreateLogger(); ``` -So far we have made `AspirationalAbp.DbMigrator`, `AspirationalAbp.EntityFrameworkCore`, and `AspirationalAbp.AuthServer` compatible with .NET Aspire. Now let's continue with `AspirationalAbp.HttpApi.Host`. +So far we have made `AspirationalAbp.DbMigrator`, `AspirationalAbp.EntityFrameworkCore`, and `AspirationalAbp.AuthServer` compatible with Aspire. Now let's continue with `AspirationalAbp.HttpApi.Host`. ### AspirationalAbp.HttpApi.Host @@ -253,7 +253,7 @@ To use the **OpenTelemetry** sink we have installed the `Serilog.Sinks.OpenTelem /// .CreateLogger(); ``` -Finally, let's make `AspirationalAbp.Web` compatible with .NET Aspire. +Finally, let's make `AspirationalAbp.Web` compatible with Aspire. ### AspirationalAbp.Web @@ -299,8 +299,8 @@ 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. +Combining 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) +* [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-08-01-abpio-platform-83-rc-has-been-published/post.md b/docs/en/Community-Articles/2024-08-01-abpio-platform-83-rc-has-been-published/post.md index 4ddd247a63..9ddf93d189 100644 --- a/docs/en/Community-Articles/2024-08-01-abpio-platform-83-rc-has-been-published/post.md +++ b/docs/en/Community-Articles/2024-08-01-abpio-platform-83-rc-has-been-published/post.md @@ -162,7 +162,7 @@ There are exciting articles contributed by the ABP community as always. I will h * [Create a Generic HTTP Service to Consume a Web API](https://abp.io/community/articles/create-a-generic-http-service-to-consume-a-web-api-yidme2kq) by [Bart Van Hoey](https://github.com/bartvanhoey) * [Use User-Defined Function Mapping for Global Filter](https://abp.io/community/articles/use-userdefined-function-mapping-for-global-filter-pht26l07) by [Liming Ma](https://github.com/maliming) -* [How to use .NET Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk) by [Berkan Şaşmaz](https://twitter.com/berkansasmazz) +* [How to use Aspire with ABP framework](https://abp.io/community/articles/how-to-use-.net-aspire-with-abp-framework-h29km4kk) by [Berkan Şaşmaz](https://twitter.com/berkansasmazz) * [Exciting New Feature in ABP.IO CMS Kit: Marked Item System](https://abp.io/community/articles/exciting-new-feature-in-abp.io-cms-kit-marked-item-system.-2hvpq0me) by [Suhaib Mousa](https://abp.io/community/members/suhaibmousa032@gmail.com) 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/create) to the ABP Community. diff --git a/docs/en/Community-Articles/2024-08-12-join-abp-at-net-conf-focus-on-ai/post.md b/docs/en/Community-Articles/2024-08-12-join-abp-at-net-conf-focus-on-ai/post.md index 432ece093c..f1a4f979fa 100644 --- a/docs/en/Community-Articles/2024-08-12-join-abp-at-net-conf-focus-on-ai/post.md +++ b/docs/en/Community-Articles/2024-08-12-join-abp-at-net-conf-focus-on-ai/post.md @@ -17,7 +17,7 @@ Get ready for deep dives into AI integration, practical examples, and insights f ### Session Highlights * **Getting Started with AI in .NET** by *Stephen Toub* -* **NET Aspire and Semantic Kernel** by *Steve Sanderson* and *Matthew Bolanos* +* **Aspire and Semantic Kernel** by *Steve Sanderson* and *Matthew Bolanos* * **Interactive AI with Blazor and .NET** by *Dan Roth* * **AI Models in .NET: From Local to Cloud** by *Bruno Capuano* * **RAG with .NET and Azure SQL** by *Davide Mauri* diff --git a/docs/en/Community-Articles/2024-10-01-SignalR-9-New-Features/Post.md b/docs/en/Community-Articles/2024-10-01-SignalR-9-New-Features/Post.md index c92e38f764..e662dd5231 100644 --- a/docs/en/Community-Articles/2024-10-01-SignalR-9-New-Features/Post.md +++ b/docs/en/Community-Articles/2024-10-01-SignalR-9-New-Features/Post.md @@ -44,7 +44,7 @@ public class MyHub : Hub ### Better Diagnostics and Telemetry -Microsoft focuses mainly on .NET Aspire nowadays. That’s why SignalR now integrates more deeply with the .NET Activity API, which is commonly used for distributed tracing. The enhancement is implemented for better monitoring in [.NET Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#using-the-dashboard-with-net-aspire-projects). To support this feature: +Microsoft focuses mainly on Aspire nowadays. That’s why SignalR now integrates more deeply with the .NET Activity API, which is commonly used for distributed tracing. The enhancement is implemented for better monitoring in [Aspire Dashboard](https://aspire.dev/dashboard/overview/). To support this feature: 1- Add these packages to your`csproj`: @@ -76,9 +76,9 @@ builder builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); ``` -Finally, you’ll see the **SignalR Hub** events on the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview): +Finally, you’ll see the **SignalR Hub** events on the [Aspire Dashboard](https://aspire.dev/dashboard/overview/): -![.NET Aspire Activity Dashboard](signalr-activity-dashboard.png) +![Aspire Activity Dashboard](signalr-activity-dashboard.png) 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 index fe7b706b5a..db3eacbd9c 100644 --- 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 @@ -1,18 +1,18 @@ -# .NET Aspire vs ABP Studio: Side by Side +# 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. +In this article, I will compare [Aspire](https://aspire.dev/) by [ABP Studio](https://abp.io/docs/latest/studio) by explaining their similarities and differences. ## 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. +While 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. +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 Aspire and ABP Studio. -### What is .NET Aspire? +### What is 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**. +**[Aspire](https://aspire.dev/)** 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: +Here, a screenshot from [Aspire dashboard](https://aspire.dev/dashboard/overview/) that is used for application monitoring and inspection: ![dotnet-aspire-dashboard](dotnet-aspire-dashboard.png) @@ -26,7 +26,7 @@ Here, a screenshot from the ABP Studio [Solution Runner panel](https://abp.io/do ## 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: +Before deep diving details, I want to show a **table of features** to compare ABP Studio and Aspire side by side: ![abp-studio-vs-net-aspire-comparison-table](abp-studio-vs-dotnet-aspire-comparison-table.png) @@ -36,30 +36,30 @@ In the next sections, I will go through each feature and explain differences and ### 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 has tens of integration packages to 3rd-party libraries and services. 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. +* **Aspire**'s integrations (like [MongoDB](https://aspire.dev/integrations/databases/mongodb/), [RabbitMQ](https://aspire.dev/integrations/messaging/rabbitmq/), [Dapr](https://aspire.dev/integrations/frameworks/dapr/), etc), on the other hand, for simplifying configuration, service discovery, orchestration and monitoring of these tools within Aspire host. Basically, these are mostly for **integrating to 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. +On the other hand, Aspire's [MongoDB](https://aspire.dev/integrations/databases/mongodb/) integration basically adds [MongoDB driver library](https://www.nuget.org/packages/MongoDB.Driver/) to your 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 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. +Both of ABP Studio and 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. +* Aspire's [project templates](https://aspire.dev/get-started/aspire-sdk-templates/)' main purpose is to provide a minimal application structure that is **pre-integrated to 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. +So, when you start with 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. +Monitoring applications and services is an important requirement for building **complex distributed systems**. Both of ABP Studio and 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. +* Aspire's [dashboard](https://aspire.dev/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. @@ -85,7 +85,7 @@ Here a screenshot that shows how to add new microservices, API gateways or web a ![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. +Aspire has no such a feature and has no such a plan to provide that kind of architectural solution building experience. ### Kubernetes Integration @@ -99,11 +99,11 @@ Here, a few tasks you can accomplish using ABP Studio's Kubernetes integration: * **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. +ABP Studio's Kubernetes Integration makes microservice development so easy and comfortable. On the other hand, 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). +Until now, I directly compared ABP Studio and Aspire features. 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: @@ -111,26 +111,26 @@ The following diagram shows ABP Platform components at a glance: 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 +## ABP and 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. +I have a good news to you. It is actually possible and pretty easy to make ABP Platform and 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)**. +You can check [@berkansasmaz](https://abp.io/community/members/berkansasmaz)'s great article: **[How to use 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. +On the other hand, 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. +Both Aspire and ABP Studio serve distinct purposes, catering to different types of development environments. While 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. +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 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) +* [Aspire documentation](https://aspire.dev/) +* [How to use 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-23-abpio-platform-90-rc-has-been-published/post.md b/docs/en/Community-Articles/2024-10-23-abpio-platform-90-rc-has-been-published/post.md index a7dfa896bd..3cdec2269e 100644 --- a/docs/en/Community-Articles/2024-10-23-abpio-platform-90-rc-has-been-published/post.md +++ b/docs/en/Community-Articles/2024-10-23-abpio-platform-90-rc-has-been-published/post.md @@ -205,7 +205,7 @@ There are exciting articles contributed by the ABP community as always. I will h * [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) +* [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/) 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 index 174aca756d..91008416df 100644 --- 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 @@ -1,27 +1,27 @@ -# .NET Aspire 9.0 Features +# 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. +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 +## Upgrade to 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. +Now, you don't need workloads to develop Aspire applications. In your project, you can add an SDK reference to `Aspire.AppHost.Sdk`. +For more information, you can check out [https://aspire.dev/whats-new/aspire-9/#upgrade-to-aspire-9](https://aspire.dev/whats-new/aspire-9/#upgrade-to-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; +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. +* **Mobile and responsive support**: The 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. +* **Health checks**: 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. +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. @@ -32,23 +32,23 @@ For more information, you can check out [https://learn.microsoft.com/en-us/dotne ## 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; +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. +Aspire has integrations with some services and tools that make it easy to get started. New integrations are coming with 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. +For Azure part, it is better to check the official documentation here [https://aspire.dev/whats-new/aspire-9/#important-azure-improvements](https://aspire.dev/whats-new/aspire-9/#important-azure-improvements) 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). +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 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/2025-11-30-NET-Conf-China-2025/POST.md b/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/POST.md index 6f8dfb96a1..94602b4188 100644 --- a/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/POST.md +++ b/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/POST.md @@ -10,9 +10,9 @@ This year’s conference focused on three main themes: performance improvements, ### Opening Keynote -Scott Hanselman kicked off .NET Conf China 2025 with a video keynote, announcing that .NET 10 is now available on the official website. He framed the release around four pillars—AI, cloud-native, cross-platform, and performance—including integration with the Microsoft Agent Framework for building and orchestrating multi-agent systems in .NET/C#, industry-leading container and Kubernetes support with .NET Aspire simplifying local containerized development, a richer cross-platform desktop ecosystem (.NET MAUI, Avalonia, Uno Platform), and major performance gains such as Native AOT and single-file publishing for faster startup and easier distribution across platforms. +Scott Hanselman kicked off .NET Conf China 2025 with a video keynote, announcing that .NET 10 is now available on the official website. He framed the release around four pillars—AI, cloud-native, cross-platform, and performance—including integration with the Microsoft Agent Framework for building and orchestrating multi-agent systems in .NET/C#, industry-leading container and Kubernetes support with Aspire simplifying local containerized development, a richer cross-platform desktop ecosystem (.NET MAUI, Avalonia, Uno Platform), and major performance gains such as Native AOT and single-file publishing for faster startup and easier distribution across platforms. -He underscored China’s importance as .NET’s second-largest market, with roughly 13% of users, and noted that generative AI usage in China has doubled in 2025. The local community is seeing strong momentum around ML.NET, .NET Aspire, and the C# Dev Kit in VS Code. Reflecting on his Baby Smash game written 20 years ago, which now runs cross-platform on .NET 10, he called on developers to modernize: move existing Web, WinForms, and WPF apps to the cloud, improve performance, ship as a single executable, and weave in AI capabilities. +He underscored China’s importance as .NET’s second-largest market, with roughly 13% of users, and noted that generative AI usage in China has doubled in 2025. The local community is seeing strong momentum around ML.NET, Aspire, and the C# Dev Kit in VS Code. Reflecting on his Baby Smash game written 20 years ago, which now runs cross-platform on .NET 10, he called on developers to modernize: move existing Web, WinForms, and WPF apps to the cloud, improve performance, ship as a single executable, and weave in AI capabilities. On AI, he emphasized a human-centered stance: AI and agents should augment, not replace, developers. In the future, developers will orchestrate and govern agents, and human judgment will matter more than ever. He closed by thanking the open-source community for its many proposals and pull requests, stressing that .NET is an open-source platform built together by Microsoft and the community, and wishing everyone an inspiring conference and a joyful journey with .NET 10. @@ -28,7 +28,7 @@ The roundtable discussion, titled “Empowering with AI, Breaking Through Cross- The afternoon featured four breakout sessions, covering a wide range of topics with deep dives into both foundational technologies and real-world project reviews: -- **Frontend and Cross-Platform:** Focused on the progress of Avalonia, Blazor, and WebAssembly, as well as the integrated experience of .NET Aspire in multi-service applications. Speakers shared insights on reusing core logic between desktop and web, shortening cold start times with incremental compilation and resource trimming, and performance profiling and optimization in WASM scenarios. +- **Frontend and Cross-Platform:** Focused on the progress of Avalonia, Blazor, and WebAssembly, as well as the integrated experience of Aspire in multi-service applications. Speakers shared insights on reusing core logic between desktop and web, shortening cold start times with incremental compilation and resource trimming, and performance profiling and optimization in WASM scenarios. - **AI Agents and Enterprise Adoption:** Discussed multi-agent orchestration, the MCP plugin ecosystem, and enterprise data compliance. From common pitfalls of “demo-level” AI to the “five-step method” for moving from POC to production, the session covered use cases like knowledge retrieval, process automation, intelligent customer service, and developer assistants, emphasizing evaluation metrics, prompt engineering, and monitoring governance. - **.NET Practices and Engineering:** Focused on the latest capabilities and performance practices of EF Core, the boundaries of NativeAOT, automated testing strategies, and observability implementation. Discussions included database migration strategies, caching and concurrency control for hot paths, end-to-end tracing, and structured logging. - **Solutions and Case Studies:** From Clean Architecture/DDD to AI-powered business evolution, topics included application modernization, SaaS transformation, and edge-cloud collaboration in AIoT. Speakers broke down modular governance, team collaboration, and release strategies for complex systems, putting “delivering value continuously” at the center stage. diff --git a/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/Post.md b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/Post.md new file mode 100644 index 0000000000..35e23f7ccb --- /dev/null +++ b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/Post.md @@ -0,0 +1,511 @@ +If you come from plain ASP.NET Core and open your first ABP solution, the initial reaction is often the same: *why are there so many projects, layers, DTOs, interfaces and base classes just to build a simple feature?* 🤔 + +That reaction is normal 👌 + +ABP Framework gives you a lot on day one: modularity, DDD-friendly structure, application services, repositories, auto API controllers, authorization, auditing, multi-tenancy and UI integration patterns. +The upside is speed and consistency on serious business apps. +The downside is that beginners can hit an abstraction wall before they see the payoff. + +After reviewing recent discussions, one pattern is clear: most developers do not struggle with C# or ASP.NET Core itself. +They struggle with *where code is supposed to go* in ABP and *which parts are essential versus optional*. + +In this post, I'll focus on that gap. If you are new to ABP, here is what actually helps. + +## The biggest learning barrier is not syntax! It is responsibility boundaries + +The hardest part for most newcomers is not learning one more framework API. +It's understanding the architectural split: + +- What belongs in the **Domain** layer? +- What belongs in **Application** services? +- Why do **DTOs** exist if you already have entities? +- When do you need a **repository**? +- Why are there separate projects like `Application.Contracts`, `Domain.Shared` and `EntityFrameworkCore`? + +In plain ASP.NET Core apps, many developers put a lot of this logic in controllers, services or even EF Core models. +ABP forces you toward clearer separation. + +A practical mental model: + +- **Entity / Aggregate Root**: business state and core invariants +- **Domain Service**: domain logic that does not naturally belong to a single entity +- **Repository**: persistence access for aggregates +- **Application Service**: use-case orchestration, authorization, DTO mapping, transaction boundary +- **DTO**: data contract for input/output +- **UI / API layer**: presentation concerns only + +> That sounds clean on paper... +> The confusion starts when you build something real 🥴 + +### A simple example: where should validation go? + +Suppose you are creating an `Order`. + +- If the rule is "order total must be greater than zero," **that's domain logic**. +- If the rule is "only users with the Orders.Create permission can create an order," **that belongs in the application layer**. +- If the rule is "customer name is required on this page," **that may exist in DTO validation too**. + +New ABP developers often ask which layer owns relationships, validation and business rules. + +> The honest answer is: different validation lives in different places. + +That is the first ABP lesson worth learning👍 + + + +![Generated illustration](abp-layers.png) + +## Why ABP Feels Heavy at First! + +**ABP is opinionated**. It's not trying to be the thinnest possible wrapper over ASP.NET Core. + +What beginners usually experience as "**too much structure**" comes from 4 things: + +### 1. Project and layer count + +A typical ABP solution can include: + +- `Domain` +- `Domain.Shared` +- `Application` +- `Application.Contracts` +- `EntityFrameworkCore` +- `HttpApi` +- `HttpApi.Client` +- UI project such as MVC, Razor Pages, Blazor or Angular +- Test projects + +For a small feature, that can feel excessive... **For a long-lived business system, it starts to make sense**. + +### 2. Generated convenience hides the mechanics + +ABP can generate a lot of CRUD plumbing and generic base classes like `CrudAppService` reduce repetitive code. +That's useful, but it can also hide how things connect. + +A beginner sees a working page and API without fully understanding: + +- how the application service is exposed as an API +- where repository methods are coming from +- how DTO mapping works +- why the UI calls application contracts instead of entities + +### 3. DDD terminology raises the entry cost + +You do not need to become a DDD master to use ABP well... But ABP definitely assumes some familiarity with: + +- entities +- aggregate roots +- repositories +- value objects +- domain services +- bounded contexts and modules + +If those ideas are new, ABP can feel harder than it really is. + +### 4. UI integration is not always obvious + +Newcomers also get stuck on the end-to-end flow: + +1. User clicks a button on a Razor Page or Blazor page +2. UI sends data to an application service or HTTP API +3. Application service validates permissions and input +4. Domain and repository code runs +5. DTO comes back to the UI + +Once you understand that flow, ABP becomes much more predictable. + +## Start with CRUD, but do not stop there + +A common question is whether beginners should start with simple CRUD or jump straight into a realistic business module. + +My view: **start with CRUD, then quickly move to a business feature with real rules**. + +### Why CRUD is the right first step + +CRUD teaches the ABP basics with low cognitive load: + +- project structure +- entity definition +- DTOs +- repositories +- application services +- permissions +- UI page wiring +- migrations and database updates + +This is why the [ABP BookStore tutorial](https://abp.io/docs/latest/tutorials/book-store) is a useful starting point. + +### Why CRUD alone is not enough + +Pure CRUD can give you a false sense of understanding. + +A generated Create / Read / Update / Delete screen does not force you to deal with: + +- aggregate boundaries +- child collections +- business invariants +- cross-entity rules +- domain services +- richer authorization scenarios +- multi-tenancy behavior +- auditing decisions + +Those are the areas where ABP starts to justify its structure. + +### A better learning sequence + +Use this progression: + +1. Build one very small CRUD module +2. Rebuild part of it manually instead of relying only on generation +3. Build one realistic business module with at least one non-trivial rule +4. Add authorization, validation and a relationship +5. Add tests around the domain or application service + +That path keeps the early win while exposing the real architecture. + + + +![Generated illustration](crud-vs-manual.png) + +## Generated CRUD vs manual CRUD: learn both + +This is one of the most useful mindset shifts for ABP beginners. + +**Generated CRUD is for productivity. Manual CRUD is for understanding.** + +You need both. + +### When generated CRUD helps + +ABP Suite and ABP base services can save time when the feature is mostly standard admin functionality: + +- back-office reference data +- simple management screens +- low-risk maintenance pages +- conventional DTO/entity flows + +If the goal is shipping business software efficiently, generated code is not cheating. It is leverage. + +### When manual implementation matters + +You should manually implement at least one feature end to end so you understand: + +- how `CrudAppService` reduces boilerplate +- what repository methods are doing +- where validation belongs +- how authorization is applied +- how auto API controllers expose application services + +A lot of Reddit confusion around ABP comes from learning generated patterns before understanding the underlying manual version. + +### A good exercise + +Build `Product` management twice: + +- First with `CrudAppService` +- Then manually with custom application service methods and domain rules + +Compare both implementations. That single exercise teaches more than reading docs for hours. + +## Which DDD patterns real ABP teams often simplify + +This is where many beginners get relief: **not every ABP project uses full-strength DDD all the time.** + +Real teams often simplify the model, especially early on. + +### Patterns teams commonly keep + +These tend to deliver value quickly in ABP: + +- clear application service boundaries +- entities and aggregate roots +- repositories +- DTO separation +- modular structure +- permission-based authorization + +### Patterns teams often delay or reduce + +These are useful in the right context, but many teams do not force them into every feature: + +- dedicated domain services for very simple logic +- value objects for every tiny concept +- specification pattern everywhere +- excessive interface layering where no variation is expected +- over-splitting modules too early + +### A practical rule of thumb + +Use the simplest thing that preserves clarity. + +For example: + +- If a rule is trivial and local to one use case, putting it in an application service may be fine. +- If a rule protects business invariants and must hold regardless of caller, move it into the domain model. +- If a concept has behavior and invariants of its own, a value object may help. +- If it is just a shared enum or constant, `Domain.Shared` is often enough. + +ABP supports rich DDD patterns, but it does not require ceremony for ceremony's sake. + + + +![Generated illustration](abp-learning-path.png) + +## A concrete “ASP.NET Core to ABP” learning path + +If I had to design a practical learning path for experienced ASP.NET Core developers, it would look like this. + +### Step 1: Know what ABP is adding on top of ASP.NET Core + +Before touching templates, be comfortable with: + +- dependency injection +- configuration +- middleware basics +- EF Core or MongoDB +- controllers or Razor Pages or Blazor basics +- validation and authorization in ASP.NET Core + +> ABP builds on top of these. It does not replace the need to understand them. + +### Step 2: Learn the ABP solution structure + +Let's see each layer's goal: + +- `Domain`: core business model +- `Domain.Shared`: shared enums, constants, localization resources, simple shared types +- `Application.Contracts`: DTOs and service contracts +- `Application`: use cases and orchestration +- `EntityFrameworkCore`: database mappings and repository implementation details +- `HttpApi`: API exposure +- UI project: user interaction + +### Step 3: Understand modules and dependencies + +ABP's modularity is a major feature, but beginners often treat modules like folders with extra steps. + +They are more than that. + +A module defines: + +- dependency boundaries +- service registration scope +- reusable feature packaging +- initialization points via module lifecycle methods and `[DependsOn]` + +At first, use modules as organizational boundaries inside a modular monolith. Do not rush into distributed or microservice-style decomposition. + +### Step 4: Build one CRUD feature the ABP way + +Create a simple feature such as Books, Products or Categories. + +Make sure you understand: + +- entity creation +- migration flow +- DTO mapping +- application service methods +- permission checks +- how the UI or API calls the application layer + +### Step 5: Rebuild one part manually + +Now remove the training wheels for one feature. + +Instead of only leaning on base classes, explicitly write: + +- a custom application service method +- a custom repository query if needed +- domain validation or invariants +- a tailored DTO instead of generic CRUD shapes + +This is where ABP usually clicks. + +### Step 6: Build a realistic business module + +A good example is `Order Management`, `Leave Requests` or `Inventory Transfer`. + +Choose something with: + +- one-to-many relationship +- status transitions +- authorization rules +- at least one business invariant +- audit visibility + +That reveals why ABP's layered structure exists. + +### Step 7: Add built-in ABP concerns on purpose + +ABP shines when you use its built-in platform features intentionally: + +- authorization +- auditing +- validation +- localization +- multi-tenancy +- settings and permissions + +Do not treat these as advanced extras. They are part of the framework's real value. + +### Step 8: Learn testing by layer + +Even if you do not build a full testing strategy immediately, understand the testing shape: + +- domain tests for invariants and business rules +- application tests for use cases and permissions +- integration tests for persistence and module wiring + +A lot of ABP's architecture pays off once you start testing behavior in isolation. + +## A small example of responsibility split + +Here is a deliberately small example to make the layering less abstract. + +Suppose you have a leave request system. + +**Domain** concerns: + +- a leave request cannot be approved after rejection +- end date cannot be before start date +- total leave days must be positive + +**Application** concerns: + +- only managers can approve requests +- map input DTO to entity operations +- return a DTO shaped for the UI +- coordinate repository access and unit of work + +**UI** concerns: + +- disable approve button when user lacks permission +- show validation messages +- render status badges and filters + +That split is the heart of ABP. Once you start seeing features this way, the framework becomes much easier to navigate. + +## When to use ABP and when not to + +**ABP is powerful, but it is not automatically the right default for every ASP.NET Core project.** + +### When to use ABP + +ABP is a strong fit when you are building: + +- line-of-business applications +- admin-heavy platforms +- SaaS or multi-tenant systems +- modular monoliths that may grow over time +- systems that need built-in authorization, auditing, localization and consistent conventions +- teams that benefit from standardized architecture + +### When NOT to use ABP + +ABP may be excessive in the following situations: + +- a tiny API with minimal business logic +- a short-lived internal tool where framework structure would dominate the workload +- a team with no interest in layered architecture or DDD-style thinking +- a highly custom architecture where ABP conventions would mostly be bypassed + +The main cost of ABP is not performance or syntax 🤜 It is **architectural overhead**. +If the app is too small, that overhead may not pay back. + +--- + + + +## Common mistakes new ABP developers make + +These are the mistakes I see most often in early ABP learning. + +### 1. Trying to understand everything before building anything + +Do not wait until every project, package and abstraction makes sense. Build one feature first. + +### 2. Using generated code without reading it + +Generated CRUD is useful, but inspect what it created. Otherwise you will stay dependent on tooling. + +### 3. Forcing textbook DDD into every feature + +Not every screen needs aggregates, value objects, domain services and custom repositories all at once. + +### 4. Putting all business logic in application services + +This works for a while, but you eventually lose domain consistency. Protect important invariants closer to the domain model. + +### 5. Splitting into too many modules too early + +Start with a modular monolith mindset. Extract boundaries when they become meaningful. + +### 6. Ignoring built-in ABP features + +If you manually rebuild authorization, auditing or tenant-aware behavior without understanding ABP's built-ins, you are fighting the framework. + + + +## The learning path I would actually recommend to a new team + +If a team asked me for a practical ABP onboarding sequence, I would keep it simple: + +### 📚 Week 1: Basics and orientation + +- Review ABP solution structure +- Build the BookStore-style tutorial once +- Identify what each layer is responsible for + +### 📚 Week 2: Manual feature implementation + +- Build one small module manually +- Avoid too much generation +- Trace one request from UI to application service to repository to database + +### 📚 Week 3: Real business rules + +- Add relationships +- Add authorization +- Add a workflow or state transition +- Write tests for a few business rules + +### 📚 Week 4: Productivity and conventions + +- Reintroduce generated tooling where it saves time +- Standardize module patterns +- Decide which DDD patterns the team will use by default and which are optional + +That sequence teaches both the architecture and the productivity side of ABP. + +--- + + + +## Final perspective: learn the intent, not just the template + +ABP can feel complicated when approached as a collection of projects and base classes. It gets easier when you see the intent behind the structure: + +- protect business rules +- standardize application boundaries +- make common enterprise features reusable +- keep large apps maintainable + +If you are new to ABP, do not aim to master every pattern immediately. Aim to answer these four questions clearly for each feature: + +- What is the business rule? +- Which layer owns it? +- What data crosses the boundary? +- Which ABP feature already solves part of this problem? + +Once those answers become natural, ABP stops feeling heavy and starts feeling productive. + +--- + +## As a Summary + +- **The biggest ABP learning barrier is** understanding responsibility boundaries between domain, application services, DTOs, repositories and UI. +- **Start with a small CRUD feature**, but move quickly to a realistic business module with rules, relationships and permissions. +- **Learn both generated and manual CRUD**; one gives productivity, the other gives understanding. +- Real ABP teams often simplify DDD and adopt advanced patterns **only when the complexity justifies them**. +- **The best learning path is ASP.NET Core basics first**, then ABP layers, one manual feature, one real module and built-in features like authorization and auditing. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-layers.png b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-layers.png new file mode 100644 index 0000000000..a9c0742d8a Binary files /dev/null and b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-layers.png differ diff --git a/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-learning-path.png b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-learning-path.png new file mode 100644 index 0000000000..6eec1b52f2 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/abp-learning-path.png differ diff --git a/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/cover.png b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/cover.png new file mode 100644 index 0000000000..85ea752b96 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/cover.png differ diff --git a/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/crud-vs-manual.png b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/crud-vs-manual.png new file mode 100644 index 0000000000..267dedea1a Binary files /dev/null and b/docs/en/Community-Articles/2026-08-06-tips-for-developers-new-to-abp-framework/crud-vs-manual.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/12m9D9rhnUDaiDElvNysSXw.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/12m9D9rhnUDaiDElvNysSXw.png new file mode 100644 index 0000000000..42e9188172 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/12m9D9rhnUDaiDElvNysSXw.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1479e3iBZ4il0F0tlpUQDYw.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1479e3iBZ4il0F0tlpUQDYw.png new file mode 100644 index 0000000000..bbb19e327e Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1479e3iBZ4il0F0tlpUQDYw.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1EBeje4kIw0tQ4dGrI5i18A.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1EBeje4kIw0tQ4dGrI5i18A.png new file mode 100644 index 0000000000..173e2fd966 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1EBeje4kIw0tQ4dGrI5i18A.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1U_z6zDB8GIK2oVVTbKmslg.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1U_z6zDB8GIK2oVVTbKmslg.png new file mode 100644 index 0000000000..917d49716f Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1U_z6zDB8GIK2oVVTbKmslg.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1YCRTIV3eHwgGvEOVlHBDCg.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1YCRTIV3eHwgGvEOVlHBDCg.png new file mode 100644 index 0000000000..775a3519d6 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1YCRTIV3eHwgGvEOVlHBDCg.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1Zz-6JQ0gzPq2AvN1_FjbtA.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1Zz-6JQ0gzPq2AvN1_FjbtA.png new file mode 100644 index 0000000000..d88a3055b1 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1Zz-6JQ0gzPq2AvN1_FjbtA.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1tKQUwPK70eQXvad1eiXoPA.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1tKQUwPK70eQXvad1eiXoPA.png new file mode 100644 index 0000000000..aa020fe1b5 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/1tKQUwPK70eQXvad1eiXoPA.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Cover.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Cover.png new file mode 100644 index 0000000000..7390a91856 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Cover.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Post.md b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Post.md new file mode 100644 index 0000000000..4381924b8e --- /dev/null +++ b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/Post.md @@ -0,0 +1,308 @@ +For years, many .NET teams treated core open source libraries as stable background infrastructure: useful, battle-tested, and effectively free forever. That assumption is starting to break. + +When widely used projects like IdentityServer, AutoMapper, and MediatR move toward commercial or more restrictive licensing, the discussion is no longer just about one package or one maintainer. It becomes a bigger question about how the .NET ecosystem pays for the software it depends on. + +This matters because modern .NET applications are built on layers of third-party dependencies. If one of those layers changes its pricing, support model, or license terms, the impact is not theoretical. It affects procurement, architecture, upgrade strategy, compliance, and long-term maintenance. + +## The pattern is no longer isolated + +A few years ago, licensing changes in .NET could still be dismissed as exceptions. That is getting harder. + +Several important projects now illustrate the same underlying tension: software that became critical infrastructure was often maintained with a funding model better suited to side projects than production-critical systems. + +## Duende IdentityServer is the clearest example + +The IdentityServer story is probably the most visible case in .NET. What started as a widely adopted open-source identity solution evolved into Duende IdentityServer, which requires paid licenses for production use, while free usage is limited to development, testing, personal projects, or qualifying community scenarios. + +More recently, Duende moved again with its v8 generation and introduced a more tiered licensing model, including Lite, Standard, Advanced, and Custom options, plus paid add-ons for additional capabilities. At the same time, support windows are clearly tied to .NET versions, making the product feel even more like managed commercial infrastructure than community software. + +> That is not necessarily a bad thing. Identity is security-critical software. It is expensive to maintain, expensive to support, and risky to underfund. But it does mark a major shift in expectations for teams that still think of it primarily as an OSS building block. + +## AutoMapper and MediatR point to a broader shift + +AutoMapper and MediatR are different kinds of libraries, but their direction matters just as much. + +These are not niche components. They are deeply embedded in enterprise codebases, tutorials, templates, and architectural conventions. So when they move toward dual licensing, commercial terms, or more restrictive licensing, the message is clear: **even highly popular and culturally central .NET libraries may no longer fit the old “free and permissive forever” model.** + +AutoMapper’s move away from .NET Foundation membership after adopting a non-permissive license is especially notable because it highlights a governance boundary. The ecosystem may celebrate OSS, but institutions such as the .NET Foundation still rely on clear licensing rules. Once a project changes those terms, it often changes its place in the ecosystem too. + +## Why maintainers are doing this + +The easy reaction is to call commercialization a betrayal. The more honest reaction is to admit that many maintainers have been subsidizing the industry for years. + +A project can be free for users and still very expensive for its authors. +Maintaining a popular library often means: + +![image-20260816151702299](image-20260816151702299.png) + +Once a library becomes critical infrastructure, users expect reliability similar to commercial software. But expectations usually rise faster than funding. + +That imbalance creates a predictable outcome: maintainers either burn out, slow down, seek sponsorship, or commercialize. + +--- + + + +## Open-source popularity does not automatically create sustainability + +This is the part many teams still underestimate. + +A package can have massive adoption and still be financially fragile. Downloads, GitHub stars, and conference mentions do not pay for maintenance. In fact, popularity often increases the burden without improving sustainability. + +From a maintainer’s perspective, commercialization can be a rational correction: + +- charge the organizations getting the most value +- fund long-term maintenance +- offer support contracts and SLAs +- justify time spent on roadmap work +- reduce dependence on unpaid labor + +### In other words, the move to commercial licensing is often less about greed than about replacing an unrealistic business model. + +--- + + + +## Why the community reaction is so mixed + +Even if the economics make sense, the backlash is real. And frankly, some of it is justified. + +The friction usually comes from the gap between legal reality and social expectation. + +### Legally, maintainers can often change how future versions are licensed. Socially, users feel that a trusted community dependency has changed the rules after becoming embedded in thousands of systems + +--- + + + +## What bothers teams most + +In practice, teams react to more than cost. They are reacting to uncertainty. +The common concerns are familiar: + +- unexpected licensing costs appearing in mature products +- fear of future pricing increases +- procurement delays for something developers previously installed with `dotnet add package` +- license compatibility and compliance reviews +- vendor lock-in around foundational infrastructure +- migration costs if a team decides to leave later +- concern that previously core features move behind paid tiers + +This is why the strongest reactions usually happen when the library is infrastructural rather than optional. Authentication, mapping, messaging, and mediator patterns sit close to the core of many architectures. **Replacing them is possible, but rarely cheap.** + +--- + + + +## Suddenness matters as much as pricing + +A reasonable commercial model can still create anger if the transition feels abrupt. Teams generally accept that maintainers need funding. What they do not accept as easily is: + +- vague roadmap communication +- surprise license changes +- unclear grandfathering rules +- unclear distinctions between old and new versions +- feature packaging that feels like a trap for existing users + +That trust dimension matters. In OSS, the license is not the whole relationship. Predictability is part of the product. + +--- + + + +## What this signals for the .NET ecosystem + +The larger lesson is not simply that some maintainers want to get paid. It is that the .NET ecosystem is maturing into one where critical libraries are increasingly treated like products, not just repositories. That has several consequences. + +## 1. Dependency selection is now a governance decision + +Choosing a package is no longer only a technical choice. + +Press enter or click to view image in full size + +![img](1U_z6zDB8GIK2oVVTbKmslg.png) + +This does not mean avoiding all commercially backed OSS. It means evaluating dependencies the same way you evaluate databases, cloud services, or authentication providers. + +## 2. Foundation membership and community trust will matter more + +When a project leaves a permissive governance environment, it sends a signal, even if the software remains technically strong. + +Press enter or click to view image in full size + +![img](12m9D9rhnUDaiDElvNysSXw.png) + +The .NET Foundation’s stance on permissive licensing creates a useful boundary here. It does not solve commercialization, but it helps clarify which projects still fit traditional OSS expectations. + +## 3. Forks and alternatives will become more common + +When licensing changes upset users, forks appear. That is a normal OSS response. + +Press enter or click to view image in full size + +![img](1Zz-6JQ0gzPq2AvN1_FjbtA.png) + +> A reactive fork may help teams buy time, but it does not automatically become sustainable infrastructure. + +In many cases, the fork inherits the same funding problem that triggered the original commercialization. + +--- + + + +## The practical risk for engineering teams + +The biggest mistake teams can make is treating this as community drama instead of delivery risk. + +Press enter or click to view image in full size + +![img](1EBeje4kIw0tQ4dGrI5i18A.png) + +This is especially relevant for organizations with long-lived internal platforms or multi-tenant SaaS products, where one dependency can affect dozens of services. + +--- + + + +## A realistic example + +Imagine a company running an internal platform and several customer-facing .NET applications. + +- **The identity layer uses IdentityServer.** +- **Multiple services use MediatR for application-layer orchestration.** +- **Older codebases rely heavily on AutoMapper profiles.** + +If all three become cost, licensing, or governance concerns at the same time, the company suddenly has a portfolio-level problem rather than a package-level problem. + +Press enter or click to view image in full size + +![img](1479e3iBZ4il0F0tlpUQDYw.png) + +That is architecture, budgeting, and compliance converging in one decision. + +## How teams should respond + +> Panic is not useful. Blind trust is not useful either. + +A better response is to become more deliberate about dependency management. + +--- + +## Build a dependency review habit + +For critical packages, review more than API quality. + +Press enter or click to view image in full size + +![img](1tKQUwPK70eQXvad1eiXoPA.png) + +If a package sits in authentication, authorization, persistence, messaging, or application architecture, the review should be stricter than for a small utility library. + +--- + + + +## Categorize dependencies by replacement cost + +Not every package deserves the same scrutiny. + +**A useful model is:** + +- low replacement cost: small utilities, isolated helpers +- medium replacement cost: libraries used across one bounded context +- high replacement cost: foundational cross-cutting libraries used everywhere + +Commercialization risk matters most in the third category. If replacing the library means touching every service, pipeline, or authentication flow, that risk belongs on the architecture radar early., + +--- + + + +## Budget for critical OSS + +Many companies are comfortable paying for cloud hosting but still resist paying for the libraries that shape their actual application architecture. + +That mindset is becoming outdated. + +If a dependency is business-critical, teams should assume one of these will eventually be required: + +Press enter or click to view image in full size + +![img](1YCRTIV3eHwgGvEOVlHBDCg.png) + +> You will pay somehow! +> The only real question is whether you pay proactively or reactively. + +--- + +## When to use commercially backed OSS and when not to ⛔ + +Commercialization is not automatically a reason to avoid a project. + +## ✔ WHEN TO USE IT + +**Commercially backed OSS can be a good fit when:** + +![image-20260816151255530](image-20260816151255530.png) + +Identity infrastructure is the obvious example. A mature, well-supported identity product may be worth paying for if the alternative is building and maintaining security-sensitive code yourself. + +--- + + + +## ⛔ WHEN NOT TO USE IT + +**Be cautious when:** + +![be-careful](image-20260816150848261.png) + +This is where some teams may rethink packages like object mappers or mediator frameworks. If the dependency is mostly ergonomic and the long-term governance risk is rising, simpler code may be the better tradeoff. + +--- + + + +## What this means for maintainers, companies, and the community + +The ecosystem now needs more honest expectations on all sides. + +## * For maintainers + +If your library underpins production systems, sustainability needs to be part of the strategy early. Commercialization is easier to accept when it is transparent, gradual, and communicated as part of a long-term model rather than a sudden pivot. + +## * For companies + +If your business depends on OSS, treating maintainers as an infinite free resource is no longer credible. Critical dependencies should have owners, budgets, and risk reviews. + +## * For the .NET community + +The community may need to become more selective about what it normalizes as default architecture. If a pattern depends heavily on a few centralized libraries, then a licensing change in one project can ripple widely. Simpler stacks are often more resilient. + +--- + + + +## A likely next phase for .NET OSS + +The next few years will probably bring more segmentation across the .NET ecosystem. + +Expect to see more of this: + +![image-20260816151051144](image-20260816151051144.png) + +That does not mean open source in .NET is weakening. It means the ecosystem is facing the same sustainability pressures seen elsewhere: maintenance is expensive, infrastructure software has real business value, and someone eventually has to fund it. + +The healthiest outcome is not pretending commercialization should never happen. It is making sure it happens with predictable governance, fair communication, and realistic expectations from users. + +--- + + + +## SUMMARY + +- Commercialization of key .NET libraries is a sustainability signal, not an isolated incident. +- Teams should evaluate dependencies by license, governance, support policy, and replacement cost. +- Commercial OSS can be the right choice for critical infrastructure, especially where support and security matter. +- The real risk is not paying for software; it is being surprised by cost, lock-in, or migration pressure too late. +- .NET teams should treat dependency strategy as an architectural and business decision, not just a NuGet decision. diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816150848261.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816150848261.png new file mode 100644 index 0000000000..191a3e1f83 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816150848261.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151051144.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151051144.png new file mode 100644 index 0000000000..0ee2770c7c Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151051144.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151255530.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151255530.png new file mode 100644 index 0000000000..19d4c49562 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151255530.png differ diff --git a/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151702299.png b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151702299.png new file mode 100644 index 0000000000..ba1b406c9c Binary files /dev/null and b/docs/en/Community-Articles/2026-08-10-oss-sustainability-in-.net-commercialization-of-key/image-20260816151702299.png differ diff --git a/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/Post.md b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/Post.md new file mode 100644 index 0000000000..9aa3eb6e8c --- /dev/null +++ b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/Post.md @@ -0,0 +1,601 @@ +Most monoliths do not fail because they are monoliths. They fail because they become tangled. + +That is exactly why the modular monolith is such a practical architecture for business applications. You keep the operational simplicity of a single deployment, but you organize the codebase around clear business boundaries. With ABP Studio, this approach is not an afterthought. It is built into the way you create and evolve a solution. + +In this article, I will walk through how to build a modular monolith with ABP Studio, how ABP modules fit together, where teams usually get the boundaries wrong, and how to structure your solution so it stays maintainable as it grows. + +If you are building a line-of-business app and want something more disciplined than a traditional monolith, but less expensive than microservices, this is one of the strongest options in the .NET ecosystem. + +## Why a modular monolith fits many real projects + +A modular monolith gives you: + +- one deployable application +- one main runtime host +- clear module boundaries by business capability +- the option to evolve selected modules later +- less distributed systems overhead than microservices + +That trade-off matters in real teams. Most products do not need network boundaries on day one. They need: + +- faster delivery +- simpler debugging +- less infrastructure +- a codebase that does not collapse after six months + +ABP Framework is designed around modularity. A module in ABP can own its own: + +- domain model +- application services +- database integration +- API endpoints +- UI pieces +- tests + +That makes ABP a natural fit for modular monolith architecture rather than a framework you have to bend into shape. + +## What ABP Studio creates for a modular monolith + +When you choose the Modular Monolith option in ABP Studio's New Solution Wizard, ABP creates a solution structure intended for a modern modular application. + +At a high level, you typically get: + +- `main/` for the main host application +- `modules/` for business modules +- `etc/` for shared infrastructure and configuration assets + +This is a useful default because it separates the host from the business capabilities from the start. + +A simplified layout looks like this: + +```text +src/ + main/ + MyCompany.MyProduct.Web + MyCompany.MyProduct.HttpApi.Host + modules/ + Catalog/ + MyCompany.MyProduct.Catalog.Domain + MyCompany.MyProduct.Catalog.Application + MyCompany.MyProduct.Catalog.EntityFrameworkCore + MyCompany.MyProduct.Catalog.HttpApi + MyCompany.MyProduct.Catalog.Web + Ordering/ + MyCompany.MyProduct.Ordering.Domain + MyCompany.MyProduct.Ordering.Application + MyCompany.MyProduct.Ordering.EntityFrameworkCore + MyCompany.MyProduct.Ordering.HttpApi + MyCompany.MyProduct.Ordering.Web +etc/ + docker/ + k8s/ + configs/ +``` + +The exact projects depend on your choices, but the important idea is consistent: the host app lives in `main`, and business capabilities live under `modules`. + +ABP Studio also lets you choose modules up front or add them later. That is important because most teams do not know their final module map on day one. You can start with a few strong boundaries and evolve from there. + + + +![Generated illustration](inline-1.png) + +## Understanding ABP modules in practice + +In ABP, modules are first-class building blocks. They are not just folders. + +A module typically declares dependencies using attributes such as `DependsOn`, which tells ABP how pieces should be initialized and wired together. + +A minimal example looks like this: + +```csharp +using Volo.Abp.Modularity; + +[DependsOn( + typeof(AbpDddDomainModule) +)] +public class CatalogDomainModule : AbpModule +{ +} +``` + +That may look small, but it is central to the architecture. Dependencies are explicit, and the framework uses those module relationships during startup. + +In practical terms, this gives you: + +- a consistent module lifecycle +- explicit compile-time references +- less hidden coupling +- clearer ownership boundaries + +ABP also distinguishes between framework modules and your application modules. + +- Framework modules provide infrastructure features like validation, caching, permission management, and persistence integration. +- Application modules represent your business capabilities like Catalog, Ordering, Billing, or Support. + +Structurally, they are similar. The difference is their role in the system. + +## A practical module structure that scales + +One of the most useful ABP practices is layered modules. Instead of throwing everything into a single project, you separate concerns inside each module. + +A common structure is: + +- Domain +- Application +- Infrastructure or provider-specific persistence +- HttpApi +- Web or UI +- Tests + +For example, a Catalog module may look like this: + +### Domain + +This is where business rules live: + +- entities +- value objects +- domain services +- domain events +- repository interfaces + +Keep this layer focused on business behavior, not framework plumbing. + +### Application + +This layer orchestrates use cases: + +- application services +- DTOs +- authorization checks +- transaction boundaries +- coordination across domain objects + +This is usually where external callers interact with the module. + +### EntityFrameworkCore or MongoDB + +This layer handles persistence details: + +- DbContext or Mongo collections +- repository implementations +- mappings +- migrations where relevant + +ABP supports different providers, and a module can include the provider projects it actually needs. + +### HttpApi + +This exposes the module over HTTP when needed: + +- controllers +- remote service contracts +- serialization-related setup + +### Web + +If your solution includes server-side or MVC-style UI integration, this is where UI pieces for the module can live. + +### Tests + +A solid module usually has separate tests for: + +- domain logic +- application logic +- persistence integration + +For EF Core, in-memory SQLite is a practical option for provider-level tests. For MongoDB, ephemeral test instances are a common approach. + +## Step-by-step: creating a modular monolith with ABP Studio + +The tooling matters because architecture tends to decay when it is inconvenient. ABP Studio reduces that friction. + +A practical setup flow looks like this. + +### 1. Create the solution with the Modular Monolith template + +In ABP Studio: + +- create a new solution +- choose the Modular Monolith template +- select your UI and database preferences +- decide which business modules you want to include initially + +This gives you the host app under `main` and a `modules` area for business capabilities. + +### 2. Start with business boundaries, not technical layers + +Before adding modules, identify your real capabilities. Good early candidates are usually things like: + +- Catalog +- Ordering +- Inventory +- Customer Management +- Billing + +Bad module boundaries are usually technical buckets like: + +- Utilities +- Common Business Logic +- Shared Services + +Those become dumping grounds fast. + +A simple rule helps: if a module name would make sense to a product owner, it is probably closer to the right boundary. + +### 3. Add modules incrementally + +You do not need to model the whole enterprise on day one. + +Start with two or three meaningful modules. For example: + +- Catalog manages products and pricing rules +- Ordering manages carts, orders, and order state +- Identity handles users and permissions via ABP's existing modules + +This is enough to validate your architecture without over-designing it. + +### 4. Keep each module independently understandable + +A developer should be able to open `modules/Catalog` and understand: + +- what the module owns +- what it exposes publicly +- what it depends on +- how it is tested + +If the module constantly reaches into another module's internals, the boundary is already weak. + +### 5. Wire modules through explicit dependencies + +ABP's module system encourages declaring dependencies up front. + +For example, an application layer may depend on its own domain layer and some framework modules: + +```csharp +[DependsOn( + typeof(CatalogDomainModule), + typeof(AbpDddApplicationModule) +)] +public class CatalogApplicationModule : AbpModule +{ +} +``` + +This is much healthier than hidden runtime coupling or random service lookups scattered across the codebase. + + + +![Generated illustration](inline-2.png) + +## How modules should communicate + +This is where many modular monoliths either stay clean or slowly become a distributed mess inside one process. + +In ABP, module communication generally falls into two categories: + +- synchronous communication through interfaces or public application services +- asynchronous communication through events + +Both are useful. The mistake is using one for everything. + +### Option 1: synchronous calls for direct business workflows + +Use direct service calls when: + +- one module needs an immediate answer +- the workflow is naturally request-response +- the dependency is acceptable and explicit + +Example: + +- Ordering needs to verify product availability from Catalog before creating an order line. + +In that case, a clear application service contract is often the simplest solution. + +Benefits: + +- easy to trace +- easier to debug +- strong flow control +- fewer hidden side effects + +Costs: + +- tighter coupling between modules +- dependency direction must be managed carefully + +### Option 2: events for decoupled reactions + +Use events when: + +- a module publishes something that others may react to +- the publisher should not know all consumers +- eventual consistency is acceptable + +Example: + +- Ordering publishes `OrderPlaced` +- Inventory reserves stock +- Billing starts invoicing +- Notifications sends a confirmation + +Benefits: + +- lower direct coupling +- easier to add new consumers later +- better long-term separation + +Costs: + +- debugging is harder +- side effects are less obvious +- too many events can create implicit dependencies + +A good default is simple: + +- use direct calls for core request-response flows +- use events for reactions and cross-cutting side effects + +## An example module interaction design + +Imagine a small commerce system with Catalog and Ordering modules. + +### Catalog owns + +- products +- product pricing +- availability rules + +### Ordering owns + +- carts +- orders +- order state transitions + +A clean interaction might look like this: + +1. A user places an order through Ordering. +2. Ordering calls a public Catalog service to validate selected products. +3. Ordering creates the order in its own domain. +4. Ordering publishes an order-created event. +5. Other modules react as needed. + +Notice what does not happen: + +- Ordering does not directly query Catalog tables. +- Catalog does not modify Ordering aggregates. +- Shared internal entities are not passed around freely. + +That discipline matters more than the fact that everything runs in one process. + +## Database design in a modular monolith + +A modular monolith does not force a single database strategy. + +With ABP, you can support: + +- a shared database for the whole application +- separate schemas per module +- module-specific databases in some cases + +For most teams, the best starting point is a single database with clear ownership boundaries in code. + +Why this is usually the right default: + +- simpler operations +- easier local development +- straightforward transactions +- less infrastructure overhead + +But even with one database, treat data ownership seriously. + +That means: + +- each module owns its own tables and mappings +- cross-module table access is avoided +- modules interact through services or events, not direct persistence shortcuts + +If you later decide to extract a module into a separate service, this discipline will matter far more than whether you started with one database or three. + + + +![Generated illustration](inline-3.png) + +## When to use layered modules and when not to overdo them + +ABP encourages a layered structure because it scales well, but you should still apply judgment. + +### Use layered modules when + +- the module has real business complexity +- multiple developers will work on it +- you want clear separation between domain, use cases, and persistence +- the module may grow into a reusable building block + +### Do not over-layer when + +- the module is tiny and stable +- the behavior is simple CRUD with little business logic +- extra projects would create more ceremony than clarity + +There is no prize for turning a 300-line feature into six projects. + +A useful practical rule: + +- start simple, but not sloppy +- add more structure when the module earns it + +ABP makes layered modules easy, but that does not mean every feature deserves the full treatment immediately. + +## Testing strategy for a modular monolith + +Modular architecture only pays off if modules can be tested with confidence. + +A practical testing setup includes: + +### Domain tests + +Use these for pure business rules: + +- invariants +- state transitions +- validation rules +- domain service behavior + +These should be fast and framework-light. + +### Application tests + +Use these for use cases: + +- application service behavior +- authorization checks +- DTO mapping expectations +- orchestration across domain objects + +### Persistence tests + +Use these for provider-specific concerns: + +- EF Core mappings +- repository behavior +- query correctness +- migration-related assumptions + +In ABP-based solutions, this usually means separate test projects per layer or concern. That keeps failures localized and makes refactoring safer. + +## Common mistakes that break modular monoliths + +The architecture is solid, but the failure modes are predictable. + +### 1. Fake modules with real coupling + +This is the most common problem. Teams create module folders, but the code still behaves like one giant application. + +Symptoms: + +- modules reference each other's internals +- shared entities leak everywhere +- services depend on concrete implementations across modules +- repositories are used across boundaries + +If that is happening, you have namespaces, not modules. + +### 2. A shared project that becomes a dumping ground + +Be very careful with anything named: + +- Common +- Shared +- Core +- Utilities + +Some shared infrastructure is fine. Shared business logic is often a sign that boundaries are unclear. + +Prefer: + +- duplicated tiny code over premature shared abstractions +- explicit module contracts over giant common libraries + +### 3. Overusing events + +Events are powerful, but they can hide the system's real behavior. + +If every use case fires multiple events that trigger more events, debugging becomes painful. + +Use events deliberately for decoupled reactions, not as a replacement for clear application flows. + +### 4. Choosing module boundaries by org chart or UI screens + +A screen is not necessarily a module. Neither is a department name. + +Choose boundaries based on business capability and ownership of rules and data. + +### 5. Ignoring future extraction concerns entirely + +You do not need to design for microservices from day one, but you should avoid decisions that make extraction impossible later. + +Examples: + +- direct table joins across module boundaries +- exposing internal entities everywhere +- no public contracts between modules + +ABP's modular style helps here, but only if you actually respect it. + +## Modular monolith vs microservices in ABP + +ABP supports both styles, which makes the comparison especially relevant. + +### Choose a modular monolith when + +- your team is small to medium-sized +- you want fast delivery with lower ops cost +- business boundaries exist, but independent deployment is not yet needed +- you want a cleaner architecture than a traditional monolith + +### Choose microservices when + +- modules must be deployed independently +- scaling characteristics differ sharply by capability +- organizational ownership is strongly separated +- you can absorb the cost of distributed systems complexity + +### When NOT to use a modular monolith + +Do not use it if you already know that: + +- teams need full autonomy over deployment cadence +- strict runtime isolation is required +- independent data ownership must be enforced operationally from the start + +For many products, a modular monolith is the better first architecture because it preserves optionality. You can grow into more distribution later instead of paying for it before you need it. + +## A practical path for future extraction + +One of the best reasons to build a modular monolith with ABP is that the module shape is already compatible with a more distributed future. + +That does not mean extraction is free. It never is. But you can make it realistic. + +To keep that option open: + +- keep public contracts narrow +- avoid direct database coupling across modules +- communicate through application services and events +- keep module-specific logic inside the module +- treat each module as owning its own data and rules + +If one day Ordering needs to become its own service, the work becomes an architectural transition instead of a rescue mission. + +## Recommended approach for a first real project + +If I were starting a new ABP Studio solution today, I would keep it practical. + +I would: + +- create a modular monolith solution in ABP Studio +- start with 2 to 4 meaningful business modules +- use layered modules only where the complexity justifies it +- default to a single database +- enforce module boundaries in code review +- use direct service calls first, events second +- add tests per module from the beginning + +I would avoid: + +- designing ten modules before shipping one feature +- building a giant shared library +- using events for every interaction +- leaking persistence details across modules + +That balance is usually what keeps the architecture alive after the first few sprints. + +## TL;DR + +- ABP Studio makes modular monolith architecture practical by separating the host app in `main` and business capabilities in `modules`. +- ABP modules should own their domain, application logic, persistence, APIs, and tests with explicit dependencies. +- Keep module communication intentional: direct calls for request-response flows, events for decoupled reactions. +- Start with a single deployment and usually a single database, but protect boundaries as if extraction may happen later. +- The biggest risk is not the monolith itself; it is weak module boundaries that turn the codebase back into a big ball of mud. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/cover.png b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/cover.png new file mode 100644 index 0000000000..c1e45d9a62 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/cover.png differ diff --git a/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-1.png b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-1.png new file mode 100644 index 0000000000..41ae0ba03a Binary files /dev/null and b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-1.png differ diff --git a/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-2.png b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-2.png new file mode 100644 index 0000000000..08e0631110 Binary files /dev/null and b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-2.png differ diff --git a/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-3.png b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-3.png new file mode 100644 index 0000000000..cb7fd8fcdb Binary files /dev/null and b/docs/en/Community-Articles/2026-08-13-building-a-modular-monolith-with-abp-studio/inline-3.png differ diff --git a/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Cover.png b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Cover.png new file mode 100644 index 0000000000..31b01f6dfd Binary files /dev/null and b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Cover.png differ diff --git a/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Inline.png b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Inline.png new file mode 100644 index 0000000000..9ce893732f Binary files /dev/null and b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/ABP-Studio-vs-Alternatives-Inline.png differ diff --git a/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/Post.md b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/Post.md new file mode 100644 index 0000000000..2c6fa77354 --- /dev/null +++ b/docs/en/Community-Articles/2026-09-02-abp-studio-vs-aspire-ai-ides-and-crud-generators/Post.md @@ -0,0 +1,163 @@ +You already have an IDE. You might already have Aspire. You might already have Cursor. The remaining question is how those tools handle ABP solution structure, modules, run profiles, and generated application code. + +Teams compare [ABP Studio](https://abp.io/studio) with three neighbors. This is not an “IDE replacement” debate. + +1. .NET Aspire, for describing distributed resources as code and watching them on a dashboard. +2. Generic AI IDEs (Cursor, GitHub Copilot, Claude Code), for repo-wide chat and file edits. +3. CRUD scaffolders, such as `dotnet aspnet-codegenerator` and visual Blazor builders (for example Radzen). + +Those tools are good. Keep Visual Studio, Rider, or VS Code. Studio can [open the solution in them](https://abp.io/docs/latest/studio/solution-explorer). This article is about what the ABP workflow brings together in one environment: templates, module graph, Solution Runner (Studio’s tool for starting, stopping, and inspecting applications), the ABP Agent, and [ABP Suite](https://abp.io/suite) (including React). + +## TL;DR + +Choose ABP Studio when the work is ABP: solution shape, module graph, Solution Runner, AI Agent, and Suite CRUD (including React on modern solutions). + +Keep Visual Studio, Rider, or VS Code beside it for refactor, tests, and language services. + +Add .NET Aspire when you want resource-as-code and the Aspire dashboard. Studio does not forbid it; microservice templates can wire Aspire in. + +Use a generic AI IDE as an extra editor if the team already lives there. Prefer the ABP Agent when the change needs modules, migrations, proxies, or live run telemetry. + +Tables in this article follow [official ABP documentation](https://abp.io/docs/latest). + +## If the bottleneck is ABP, not the C# file + +Studio may be a suitable choice when your team is (or will be) on the ABP Platform and you want one place to: + +- Scaffold production templates (including React Modern and Classic MVC / Blazor / Angular). +- Run many services, containers, and UIs from a [Solution Runner](https://abp.io/docs/latest/studio/running-applications) profile (the set of applications and services started together). +- Ask, plan, and implement with AI against modules, packages, migrations, and live telemetry. +- Generate layered CRUD with ABP Suite for MVC, Blazor, Angular, and React. + +If you only need a general editor, keep using your IDE. If you only need container orchestration and OpenTelemetry locally, Aspire may already be on the machine. Add Studio when the solution graph and ABP conventions are the bottleneck, not the C# file. + +## What actually sits next to Studio + +| Capability | ABP Studio + Agent | .NET Aspire | General AI IDEs (Cursor, Copilot, Claude Code, …) | Visual Studio / Rider / VS Code | +|---|---|---|---|---| +| Role | ABP solution lifecycle + AI coding in context | Cloud-native orchestration, service discovery, dashboard | Repo-wide AI edit / chat / agents | General-purpose IDE | +| ABP / DDD structure in context | Solution, modules, packages, run profiles in the [agent system context](https://abp.io/docs/latest/studio/ai-agent) | Unaware of ABP modules | Files and text; no ABP module graph | Solution explorer of projects, not ABP modules | +| ABP-shaped artifacts | Suite generates entity, app service, DTOs, repository, and UI; Agent follows those module conventions | No | Generic C# unless you prompt every ABP convention | Language services only | +| Production ABP templates | Layered, modular, microservice; Modern React or Classic UIs | Minimal host templates for Aspire | None | `dotnet new` / ABP CLI if you add it | +| Run + monitor | [Solution Runner](https://abp.io/docs/latest/studio/running-applications): start/stop apps and containers, HTTP requests, exceptions, logs, events, browse UI | Aspire dashboard: resources, logs, traces, metrics | Shell / terminal processes | Multi-project debug | +| AI that can build, migrate, generate proxies | Agent mode: files, shell, [Studio tools](https://abp.io/docs/latest/studio/ai-agent-built-in-capabilities) (`dotnet_build`, `start_applications`, `generate_csharp_proxies`, `get_exceptions`, …), MCP | Not an ABP coding agent | Shell-wrapped `dotnet` if you teach it | Copilot in the editor (generic) | +| Kubernetes | Studio Kubernetes integration for ABP solutions | Publish / deploy story for Aspire apps | No | No (unless you add tools) | +| Code edit / refactor | Agent writes code; Open with your IDE for deep refactor | Not an editor | Strong | Strongest language tooling | +| Use together? | Yes, open IDE from Studio; optional Aspire integration | Yes with ABP microservice template | Yes as an extra editor | Yes; Studio launches them | + +How to read the table: these tools stack. Studio does not replace Rider's refactorings or Aspire's resource model. Its differentiated value is the ABP-aware workflow across solution structure, modules, run profiles, and agent tools. + +Kubernetes. Studio’s Kubernetes integration is for developing against a cluster (browse and health using service names from the run profile). It is not Helm, GitOps, or your production deploy pipeline. + +## The ABP Agent vs “just ask Cursor” + +![Generic AI IDE and ABP Agent: repository context and ABP Studio context](ABP-Studio-vs-Alternatives-Inline.png) + +Cursor and Copilot are excellent at finishing a LINQ query or explaining a regex. That is not the argument. + +Ask a generic AI IDE: "Add a Product entity, make it multi-tenant, add the EF Core migration, generate the React UI." A generic AI IDE can work effectively when it has sufficient repository context and explicit project conventions. ABP Studio's differentiated value is that solution, module, run-profile, and telemetry context are available through first-class Studio capabilities. For cross-layer ABP changes, this can reduce the amount of context and manual coordination the developer must provide. + +[ABP Studio AI Agent](https://abp.io/docs/latest/studio/ai-agent) is not limited to the currently open text buffer. The session can use the solution, modules, packages, runnable apps, run profile, AI scope, and enabled tools. [ABP Suite](https://abp.io/suite) remains the CRUD generator, while the Agent is designed to work with the module graph, add a migration, start the profile, and inspect live exceptions. + +Modes: + +- Ask, read-only answers. Can search ABP documentation. +- Plan, read-only implementation plans. +- Agent, read/write files, shell, add migrations, run Studio tools, MCP, update plan steps. + +The ABP Agent fits changes where module, solution, and runtime context should remain available throughout the work. + +That is the difference versus a generic AI IDE: + +| What you ask | ABP Agent | Generic AI IDE | +|---|---|---| +| “Add an app service following ABP layering” | Receives solution, module, and ABP documentation context through Studio | Works from the repository context and instructions provided to it | +| “Why did this HTTP call fail?” | `get_requests` / `get_exceptions` / `get_logs` on the running profile | You paste a log or attach a debugger | +| “Generate C# / Angular proxies” | First-class Studio tools | A shell command if the model guesses it | +| “Start the apps, then continue” | `start_applications` / `start_containers` | Terminal + wait | +| Scope | AI scopes limit which modules the agent may touch; `.abpignore` blocks secrets | Works from the repository context and instructions provided to it | + +You still review the diff. Agent mode is execution with a permission boundary, not unsupervised production deploys. + +The [privacy boundary](https://abp.io/docs/latest/studio/ai-agent) is the session you give it: files, prompts, Studio tool output, attachments, and allowed URLs. Privacy depends on the selected model, configured provider, accessible scope, and enabled tools. [`.abpignore`](https://abp.io/docs/latest/studio/ai-agent-configuration) prevents excluded files from entering the agent context. + +## Why Aspire does not replace Studio (they stack) + +[Solution Runner](https://abp.io/docs/latest/studio/running-applications) is how you run an ABP modular or microservice tree: profiles per team, folders for apps/gateways/services, C# hosts, CLI tasks (for example Angular), Docker containers, start/stop/build, browse, health, and live HTTP / exception / log / event views. + +.NET Aspire is how many .NET teams describe distributed resources as code and watch them on a dashboard (OpenTelemetry, containers, connection strings). + +Their responsibilities overlap around local orchestration, but their primary scopes are different. Aspire focuses on distributed resource orchestration, while ABP Studio adds ABP solution templates, module workflows, Suite, and the ABP Agent. + +You do not have to pick one. ABP microservice templates can enable Aspire so AppHost starts infrastructure and services; you can still use Studio’s runner and Agent. See [Aspire integration](https://abp.io/docs/latest/solution-templates/microservice/aspire-integration). + +Pairing ABP Studio with Aspire works well when ABP solution workflows and distributed resource orchestration are both needed. + +## Suite: a CRUD slice, not a pretty grid + +[ABP Suite](https://abp.io/docs/latest/suite) generates a CRUD slice of an ABP application from an entity: domain type, repository, application service, migration, UI, tests, navigation properties, multi-tenant flag, localization keys. + +| | ABP Suite | `dotnet aspnet-codegenerator` / EF scaffolding | Visual Blazor app builders (for example Radzen) | +|---|---|---|---| +| Output | Entity through application layer + UI + optional tests | Controllers, Razor Pages, Blazor CRUD, or Minimal API endpoints against a DbContext | Blazor UI + data wiring from a database or REST source | +| UI stacks | MVC, Blazor (Blazorise or MudBlazor, detected), Angular, and React (modern solutions) | MVC views, Razor Pages, and Blazor components. No Angular. No React. | Blazor only (Server, WebAssembly, Auto) | +| ABP permissions, tenancy, audit base classes | Options on the entity ([CRUD generation](https://abp.io/docs/latest/suite/generating-crud-page)) | Does not use ABP permission and tenancy conventions | Does not use ABP permission and tenancy conventions | +| Custom code on regenerate | Customizable code [hook points](https://abp.io/docs/latest/suite/customizing-the-generated-code) for MVC, Blazor, and Angular; React pages require extra care when regenerated | Often requires manually preserving custom changes | Varies by product | +| React UI | Yes, template-based CRUD for modern React apps, including search, paging, validation, permissions, localization, and navigation properties. Registers routes and menu | No React generator in the official scaffolding set (`blazor`, `razorpage`, `view`, `controller`, `identity`, `minimalapi`) | Blazor-focused rather than React-based | + +Suite supports the official web UI stacks, including React. [ABP Agent](https://abp.io/docs/latest/studio/ai-agent) is an extra path when you want AI to evolve those pages, not a substitute for Suite React output. + +Suite is a fit when generated CRUD should follow the application's ABP layers, conventions, and selected UI stack. + +## When Studio is the right tool in the stack + +Choose ABP Studio when the work is ABP: new solution shape, module graph, run profiles, Kubernetes-connected browse, Agent that can migrate and generate proxies, Suite for CRUD. + +Keep your IDE open beside it for refactor, tests, and language services. Studio expects that. + +Add Aspire when you want resource-as-code and the Aspire dashboard. Studio does not forbid it. + +Use a generic AI IDE as an extra editor if your team already lives there. Prefer Agent for ABP-structured changes so the model is not guessing module boundaries from filenames. + +Studio works best when ABP-specific solution workflows complement your existing IDE. + +## FAQ + +### We already run .NET Aspire (AppHost + dashboard). What does Studio still do that Aspire does not? +Aspire models resources (projects, containers, connection strings, OpenTelemetry) as code. Studio models an ABP solution: modules, package installation, production templates, Suite, Kubernetes browse for ABP services, and an agent that can migrate and generate proxies. Their responsibilities overlap around local orchestration, but their primary scopes are different. You can keep AppHost; microservice templates can integrate Aspire. You do not need to replace Aspire with Studio. + +### If the team already uses Cursor or Copilot on the same repo, when is ABP Agent the better tool for a task? +When the task needs ABP structure or a live run, not only a file edit: module/package scope, `generate_csharp_proxies` / `generate_angular_proxies`, `start_applications`, or `get_exceptions` / `get_requests` / `get_logs` against the [Solution Runner](https://abp.io/docs/latest/studio/running-applications) profile. Generic IDEs can work in the same repository when given the relevant context and conventions. Agent provides first-class access to module, run-profile, and Studio tool context for changes that coordinate ABP modules and running services. + +### After Suite generates React CRUD, how should we customize the page? +For MVC, Blazor, and Angular, use Suite’s [customizable code](https://abp.io/docs/latest/suite/customizing-the-generated-code) hook points. React pages require extra care when regenerated, so keep additional React UI in files Suite does not generate or evolve it with [ABP Agent](https://abp.io/docs/latest/studio/ai-agent). Suite React generation itself is template-based, not AI. + +### Can Agent start the microservice profile, then use real HTTP/exception data in the same session? +Yes, in Agent mode with a run profile: tools include `start_applications` / `start_containers` and then `get_requests`, `get_exceptions`, `get_logs`, `get_events` ([built-in capabilities](https://abp.io/docs/latest/studio/ai-agent-built-in-capabilities)). Ask/Plan cannot mutate or start apps. If nothing is running, those telemetry tools have nothing to read. That is why “AI that sees production-like local traffic” is a Studio and Runner loop, not a chat sidebar. + +### Does Studio Kubernetes integration replace Helm, GitOps, or our cluster deploy pipeline? +No. It is for developing against a cluster (browse/health using Kubernetes service names from the run profile, manage connected services). Aspire’s publish story and your CI remain how you ship. Do not treat the Studio K8s panel as the production deployment product. + +### Can one developer live in Rider and another in VS Code while sharing the same Studio solution? +Yes. Studio holds solution/run-profile metadata; [Open with](https://abp.io/docs/latest/studio/solution-explorer) launches whatever IDE is installed. Run profiles and Agent sessions are not tied to a single editor vendor. The constraint is ABP Studio itself on the machine, not a mandate to abandon Rider or VS. + +## Next step + +[Download ABP Studio](https://abp.io/studio) · [AI Agent docs](https://abp.io/docs/latest/studio/ai-agent) · [Generate a CRUD page](https://abp.io/docs/latest/suite/generating-crud-page) · [ABP Framework](https://abp.io/framework) + +### Sources + +- [ABP Studio overview](https://abp.io/docs/latest/studio) +- [AI Agent](https://abp.io/docs/latest/studio/ai-agent) +- [AI Agent built-in capabilities](https://abp.io/docs/latest/studio/ai-agent-built-in-capabilities) +- [Solution Runner](https://abp.io/docs/latest/studio/running-applications) +- [Solution Explorer / Open with IDE](https://abp.io/docs/latest/studio/solution-explorer) +- [ABP Suite](https://abp.io/docs/latest/suite) +- [Generating a CRUD page](https://abp.io/docs/latest/suite/generating-crud-page) +- [Customizing the generated code](https://abp.io/docs/latest/suite/customizing-the-generated-code) +- [ASP.NET Core `aspnet-codegenerator`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/tools/dotnet-aspnet-codegenerator) +- [.NET Aspire overview](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) +- [Radzen Blazor Studio](https://www.radzen.com/blazor-studio/) +- [Microservice Aspire integration](https://abp.io/docs/latest/solution-templates/microservice/aspire-integration) + +Product names in this article belong to their owners. Mention is for identification in a technical comparison, not affiliation or endorsement. diff --git a/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Cover.png b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Cover.png new file mode 100644 index 0000000000..0ccad75e69 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Cover.png differ diff --git a/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Inline.png b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Inline.png new file mode 100644 index 0000000000..e4f5ec2667 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/ABP-vs-Alternatives-Inline.png differ diff --git a/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/Post.md b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/Post.md new file mode 100644 index 0000000000..93afbb03a7 --- /dev/null +++ b/docs/en/Community-Articles/2026-09-02-abp-vs-clean-architecture-templates-and-starter-kits/Post.md @@ -0,0 +1,223 @@ +Every serious .NET team hits the same whiteboard sooner or later. Do we build the multi-tenant SaaS from scratch? Grab a starter kit? Adopt a platform? + +Search “ABP vs …” and you will land on pages written by kits and tenancy libraries. Fair. Those pages exist because the question is real. They are not comparing two products of the same kind. + +You are choosing among four shapes: + +1. Build it yourself on ASP.NET Core, often with a tenancy library such as Finbuckle.MultiTenant. +2. A Clean Architecture template (Jason Taylor, Ardalis) that gives you folders, layers, and a sample feature. +3. A starter kit that copies a snapshot of production modules into your repo (fullstackhero, Brick, and similar kits). +4. A maintained application platform. Architecture, infrastructure, modules, and tools that keep moving. + +ABP competes across several decision categories rather than against one uniform product set. Some teams compare it with Clean Architecture and DDD references or starter kits because they prefer to own and assemble the application foundation themselves. Others evaluate .NET application and service frameworks such as ServiceStack, model-driven RAD platforms such as DevExpress XAF, or cross-stack SaaS platforms and starter kits. Existing ASP.NET Boilerplate applications are usually a migration and adoption consideration rather than a same-generation alternative. The table below focuses on the architecture and application-foundation decision; these other categories can still enter the same broader technology discussion. + +[ABP](https://abp.io) takes the application-platform approach. The rest of this article is about the things teams evaluate later: multi-tenancy, identity, permissions, modularity, UI (including React), and a path from modular monolith (one deployable application organized into independent modules) to microservices. + +## TL;DR + +Choose ABP when you are shipping a long-lived, multi-tenant, modular .NET product and want a maintained platform: tenancy, identity, modules, official UIs (including React), and Studio / Suite on the same stack. + +Choose a Clean Architecture template (Jason Taylor, Ardalis) when the goal is to own every architectural decision from a well-named skeleton. + +Choose a starter kit (fullstackhero, Brick, and similar) when you prefer a copy of modules in your repo on day one and you are willing to maintain that snapshot. + +Choose Finbuckle (or custom tenant middleware) when tenant resolution is the only additional capability you need on an app you already own. ABP also connects tenancy with application concerns such as identity, permissions, and audit. + +Tables in this article follow [official ABP documentation](https://abp.io/docs/latest). + +## This is for the long haul + +ABP is the recommended choice when you are building a long-lived, multi-tenant, modular .NET product. B2B SaaS. Line of business. A platform several teams will still be extending in a few years. + +You want: + +- Cross-cutting concerns (auth, tenancy, audit, jobs, localization) already solved and documented. +- Official UI options that match your team: React, Angular, Blazor, MVC, MAUI, or React Native. +- A way to add pre-built modules and, later, split a module into a service without rewriting the application code. +- Tooling (Studio, Suite, CLI) and, if you need it, commercial modules and support, without leaving the same platform. + +Yes, ABP is opinionated. The initial learning cost is real: teams need to understand modules, application services, and unit of work conventions before those conventions become productive. Then, when a module needs to become a microservice, the contracts are already there. The conventions become team-wide standards so the next project does not invent a second way to do the same thing. + +## Four shapes. One table. + +![DIY ASP.NET Core, Clean Architecture, starter kit, and ABP Platform as four different shapes](ABP-vs-Alternatives-Inline.png) + +| Capability | ABP Platform | DIY ASP.NET Core | Clean Architecture templates (Jason Taylor, Ardalis) | Starter kits (fullstackhero, Brick, and similar) | +|---|---|---|---|---| +| What you get | Maintained framework + modules + templates + tooling | Empty host and libraries you assemble | Layered/CQRS skeleton + a sample feature | A copy of modules in your repo on day one | +| Multi-tenancy | Native: [single database, database-per-tenant, or hybrid](https://abp.io/docs/latest/framework/architecture/multi-tenancy); identity and authentication/token infrastructure wired for tenants | You design isolation, filters, and cache keys, or add a library such as Finbuckle.MultiTenant | Not included | Often via a tenancy library you still own end-to-end | +| Identity, permissions, audit | Application modules + automatic tenant-data filtering, [audit logging](https://abp.io/docs/latest/framework/infrastructure/audit-logging), permission system | You compose Identity, policies, and audit yourself | Identity sample at most | Bundled in the kit; you maintain the fork | +| Modularity | First-class [module system](https://abp.io/docs/latest/framework/architecture/modularity/basics); install/uninstall packages | You invent module boundaries | Folders and layers, not a product module catalog | Modular folders; upgrades are merge/cherry-pick | +| UI | [React (Modern)](https://abp.io/docs/latest/framework/ui/react), MVC, Blazor, Angular, [React Native](https://abp.io/docs/latest/framework/ui/react-native), [MAUI](https://abp.io/docs/latest/framework/ui/maui) | You pick and integrate | Template-specific (often Angular/React/API) | Kit-specific (often one SPA) | +| Monolith → microservices | Same application contracts; HTTP/C# proxies replace in-process calls | You design the split | You design the split | Extract-a-module if the kit allows it | +| Tooling | [ABP Studio](https://abp.io/studio) (including AI Agent), [ABP Suite](https://abp.io/suite), CLI | `dotnet` CLI and your IDE | Template CLI + IDE | Kit CLI / Aspire host, varies | +| How it evolves | NuGet upgrades for the framework and modules, plus documented migrations | You upgrade every library | You copy the next template over your tree | You merge upstream into source you already customized | +| Support | Open-source core plus commercial extras and vendor support | Your team | Community | Community | + +ABP may be a suitable fit when the application needs architecture, infrastructure modules, and development tooling to remain integrated and maintainable as the product grows. + +How to read the table: templates teach structure. Starter kits give you a snapshot you own. ABP combines application architecture, infrastructure modules, and development tooling in one platform. Names in the headers are examples of the category, not a review of every repository. + +Identity, permissions, and audit. ABP ships these as application modules: permission UI, automatic filtering of tenant data, and audit logging. On plain ASP.NET Core you compose Identity, policies, and audit yourself. A Clean Architecture template usually stops at an Identity sample. A starter kit may bundle them; you then maintain that copy. + +How the stack evolves. When a customized starter kit is updated, identity and tenancy code may already contain local changes. Reconciling those changes across the files you now own can add significant maintenance work. That is a different upgrade model from updating framework packages. + +ABP uses framework and module packages, together with documented migrations. A Clean Architecture template’s next version is something you copy forward. A kit’s next version is reconciled with the snapshot you already changed. + +## Let’s be honest: what are you actually downloading? + +### What you build yourself + +ASP.NET Core is phenomenal. It is a foundation, not a complete application platform. Building from scratch gives teams maximum control, but also leaves tenant resolution, permission checks, audit logs, job plumbing, and module boundaries to the team. + +That work is legitimate if the architecture is unique. For a standard business product, it is infrastructure before the first domain feature. + +ABP sits on ASP.NET Core. You keep the Microsoft stack. ABP provides established conventions and modules for teams that prefer those decisions to be part of the platform. See [Why ABP Platform](https://abp.io/docs/latest/others/why-abp-platform). + +ABP suits teams that want application infrastructure to remain integrated with the product as it grows. + +### Clean Architecture templates + +This category includes the templates most often compared with ABP: Jason Taylor’s Clean Architecture template and Ardalis’s Clean Architecture template. They provide a good classroom and a clean slate. Their focus is architectural structure and ownership rather than a built-in application module catalog or ABP-specific tooling. + +Choose a Clean Architecture template when architectural ownership and a focused starting point matter most. + +ABP is a fit when architectural conventions and application modules need to evolve together. + +### Starter kits + +A starter kit copies identity, tenancy, auditing, and a few domain modules into your repository. You own every line. That is a real preference for some teams. + +Starter kits are another common option in this decision. fullstackhero is a copy-and-own MIT kit (identity, tenancy, React admin, modules in your repo). Other starter kits, such as Brick, can also provide a prebuilt starting point; the exact features, ownership model, and update process depend on the kit. They can provide a fast starting point, but long-term maintenance depends on how each kit handles updates to customized code. + +With ABP, framework and module updates are distributed through packages and documented migrations. With a kit, teams have direct ownership of the source, while updates may require reconciling local changes with upstream code. These are different maintenance models. + +Choose a starter kit when an existing set of application features and direct source ownership can accelerate your team. + +Choose ABP when you want those application concerns, modules, and tooling to evolve as part of a maintained platform. ABP’s open-source core is yours to use; your business code lives in your repo. There is no separate proprietary runtime for that core. Commercial extras (themes, Pro modules, Suite, support) are optional layers on the same platform, not a rewrite. + +Package-based framework and module updates may suit teams that prefer them to reconciling changes in an owned source snapshot. + +## If you last looked a year ago: React is official + +This is easy to miss if you last looked at ABP a year ago. + +Official UI options ([ABP UI](https://abp.io/docs/latest/framework/ui)): + +- React, in the [Modern template system](https://abp.io/docs/latest/solution-templates/modern-vs-classic) (ABP Studio or `abp new --modern`). +- MVC / Razor Pages, Blazor (WebAssembly, Server, WebApp), and Angular, on Classic templates. +- React Native and MAUI for mobile / hybrid. + +[Modern vs Classic](https://abp.io/docs/latest/solution-templates/modern-vs-classic): Classic is not a deprecated track. It is the actively supported family with the broadest UI matrix (MVC, Angular, Blazor, MAUI) and the template-first Studio flow. Modern is the newer architecture-first Studio flow, React (or no UI), and the Admin Console / Low-Code path (metadata-driven screens configured through a designer). Choose Classic when the team’s UI is MVC, Angular, or Blazor. Choose Modern when the web UI is React. + +One backend, the UI your team already knows. + +## Multi-tenancy without a side project + +ABP treats tenancy as infrastructure, not a feature you bolt on: + +- Current tenant is resolved per request. +- Entities implementing `IMultiTenant` (ABP’s contract for separating tenant-owned data) are automatically filtered; new records get a `TenantId` identifying the tenant. +- You can use one database for all tenants, one database per tenant, or a hybrid. + +That isolation also applies to cache and related concerns so business code stays mostly tenancy-agnostic. Details: [Multi-Tenancy](https://abp.io/docs/latest/framework/architecture/multi-tenancy). + +A tenancy library on raw ASP.NET Core, including Finbuckle.MultiTenant, can resolve the tenant. It does not automatically give you permission UI, audit, jobs, Identity/OpenIddict authentication and token wiring for tenants, and module installs on the same model. That is the platform difference. Pages titled “Finbuckle vs ABP vs custom” are answering a library question. This article is answering the application-platform question. + +ABP is worth considering when tenancy needs to stay connected to identity, permissions, data filtering, and the rest of the application model. + +## Modularity that can become microservices + +ABP modules are real packages (domain, application, HTTP API, UI) with documented dependency rules. You can start with a [modular monolith](https://abp.io/architecture/modular-monolith) and later replace in-process calls with HTTP using the same application service contracts and [client proxies](https://abp.io/docs/latest/framework/api-development/dynamic-csharp-clients) (typed clients that call application services over HTTP). + +That path is the point. You do not throw away the monolith to “do microservices”; you change the hosting of a module that already had a boundary. + +This approach fits teams that want modular boundaries to support both a modular monolith and a later service split. + +## ABP Low-Code: admin screens inside the same app + +[ABP Low-Code](https://abp.io/docs/latest/low-code) is a module in your ABP application (Team license or higher). You model entities, pages, forms, permissions, and scripts in the Admin Console. The runtime uses that metadata in the same app, with the same identity, audit, and EF Core model. There is no separate low-code database. + +Designer, [Studio AI Agent](https://abp.io/docs/latest/studio/ai-agent), and hand-written C# / Script API land on one model. + +The documented runtime UI is React. You can still keep MVC, Razor Pages, Angular, or Blazor as the main UI and host the React Low-Code runtime beside it ([non-React integration](https://abp.io/docs/latest/low-code/non-react-ui-integration)). + +Docs still label the system Preview (APIs and designer may change before GA). That is a maturity label, not a missing product. + +| | ABP Low-Code | External low-code platform | Hand-written CRUD every time | +|---|---|---|---| +| Where it runs | Inside your ABP app | Separate product / runtime | Your repo | +| Data and identity | Same database and ABP authorization | Often a parallel model | Whatever you build | +| Extend with code | C#, Script API, Agent | Platform limits | Always code | +| Screens | Grid, form, calendar, kanban, gallery, dashboard, import/export | Vendor widgets | Custom pages | +| How you enable it | Studio modern wizard (layered, single-layer, modular monolith + EF Core) or [add to an existing EF Core solution](https://abp.io/docs/latest/low-code/add-to-existing-solution) | Vendor onboarding | Always code | + +Studio’s *new-solution wizard step* is omitted for microservice architecture and when MongoDB is selected, because runtime-managed tables use EF Core. Layered and modular-monolith EF Core solutions are the documented enablement path today. + +Use Low-Code for the admin CRUD and metadata-driven screens. Keep distinctive product UX in normal ABP UI, including Suite-generated React CRUD on modern solutions. + +## When this is the right call + +Pick ABP when: + +- The product will have tenants, roles, audit, and more than one team. +- You want React *or* Blazor *or* Angular *or* MVC on a shared backend. +- You expect to grow from modular monolith to services without a rewrite. +- You want Studio, Suite, and (optionally) Low-Code on the same stack. + +A Clean Architecture template is a solid choice when architectural ownership and a focused starting point matter most. + +A starter kit is a solid choice when an existing set of application features and direct source ownership can accelerate your team. ABP is a better fit when those application concerns, modules, and tooling should evolve as part of a maintained platform. + +ASP.NET Boilerplate is the predecessor, not a same-generation starter kit. If that is the comparison, it is a migration to the current ABP platform, not a fork-and-own kit decision. + +DevExpress XAF and similar commercial frameworks follow a model-driven RAD approach built around a vendor UI ecosystem. That approach can reduce application boilerplate and accelerate internal business applications, while also placing the application’s model and much of its UI experience within DevExpress conventions and controls. ABP is an application platform on ASP.NET Core, with an open-source core, modular architecture, and multiple UI options. It gives teams more direct control over the application layers and frontend choices, including MVC, Blazor, Angular, and React, while supporting a modular-monolith-to-microservices path. XAF may be the better fit when the priority is delivering a model-driven business application quickly within the DevExpress ecosystem. ABP may be the better fit when the product needs ASP.NET Core, a more customized frontend experience, multiple client options, or a modular architecture that can evolve toward services. + +## FAQ + +### Is ABP free or commercial? +ABP Framework has an open-source core. Some themes, modules, generators, Studio capabilities, support, and other commercial offerings require an appropriate commercial license. Exact availability depends on the product and license level. + +### If we stop paying for commercial extras, or we leave ABP later, is the app a black box? +No. The application is still ASP.NET Core, your C#, and EF Core (or MongoDB). The [open-source framework](https://abp.io/docs/latest/others/why-abp-platform) stays in the solution as packages you already reference. Leaving ABP means replacing those packages the way you would replace any framework, not extracting code from a closed runtime. Suite output is source in your repo. Commercial modules, themes, and Suite itself are optional layers; dropping them does not delete your domain. + +### If we already have a Clean Architecture or starter-kit repo (Jason Taylor, Ardalis, fullstackhero, Brick), can we “just add ABP” like a NuGet? +No. ABP’s module pipeline, interceptors, automatic tenant-data filters (which limit queries to the current tenant), and startup conventions assume it is the host. Teams adopt ABP on a new solution (or plan a migration), they do not drop `Volo.Abp.*` onto an existing CA template and keep the old composition root unchanged. That is the real cost versus a tenancy library you add to code you already own. See [Why ABP Platform](https://abp.io/docs/latest/others/why-abp-platform). + +### We already use Finbuckle (or custom tenant middleware). What does ABP tenancy still do that that library does not? +A tenancy library resolves *who the tenant is*. ABP also isolates queries and inserts (`IMultiTenant` data filters), wires Identity and OpenIddict for tenants, and applies the same tenant context to cache and related infrastructure. That covers [single database, database-per-tenant, or hybrid](https://abp.io/docs/latest/framework/architecture/multi-tenancy). Permissions, audit, and jobs then run inside that model instead of each being a separate integration project. + +### If we start Modern/React, can the same backend later serve a second UI (public MVC, partner Blazor, mobile)? +Yes. Application services and HTTP APIs are UI-agnostic. [Official UIs](https://abp.io/docs/latest/framework/ui) (React, Angular, Blazor, MVC, React Native, MAUI) are hosts on that backend. You choose Modern vs Classic at solution creation for the primary web app; extra clients consume the same APIs. You do not rewrite domain or application layers to add a second frontend. + +### When we split a module into a microservice, do we rewrite application services as controllers? +Not as the default path. Module contracts stay; in-process calls become HTTP (or messaging) via [client proxies](https://abp.io/docs/latest/framework/api-development/dynamic-csharp-clients) against the same application-service interfaces. You change hosting, not the feature’s application API. That is the comparison point versus a starter kit whose “modules” are folders with no proxy story. + +### For the same admin entity, should we use Suite-generated React CRUD or Low-Code pages? +[Suite](https://abp.io/docs/latest/suite) writes source into your layers (entity, app service, UI, tests) that you commit and customize, including React on modern solutions. [Low-Code](https://abp.io/docs/latest/low-code) keeps the entity in metadata (designer + runtime APIs/pages) without generating those classes for the standard flow. Use Suite when the screen will grow into product code; use Low-Code when the screen should stay designer-driven. They are not substitutes for each other. + +### Can we keep MediatR / vertical-slice handlers for new features inside an ABP solution? +You can reference extra libraries, but ABP’s default application surface is [application services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services) with unit of work, validation, and authorization conventions. Comparison with CA templates is not “MediatR vs nothing”; it is whether the team standardizes on ABP’s application layer or on a handler-per-feature folder as the public API. Mixing both without a rule usually duplicates the same use case in two styles. + +## Next step + +Create a solution in [ABP Studio](https://abp.io/studio) and pick Modern (React) or Classic (MVC / Blazor / Angular). Then add only the modules you need. + +[Create a free ABP solution](https://abp.io/studio) · [Read the framework overview](https://abp.io/framework) · [Why ABP Platform](https://abp.io/docs/latest/others/why-abp-platform) + +### Sources + +- [ABP UI options](https://abp.io/docs/latest/framework/ui) +- [Modern vs Classic templates](https://abp.io/docs/latest/solution-templates/modern-vs-classic) +- [React UI](https://abp.io/docs/latest/framework/ui/react) +- [Multi-tenancy](https://abp.io/docs/latest/framework/architecture/multi-tenancy) +- [Why ABP Platform](https://abp.io/docs/latest/others/why-abp-platform) +- [Low-Code System](https://abp.io/docs/latest/low-code) +- [Jason Taylor Clean Architecture template](https://github.com/jasontaylordev/CleanArchitecture) +- [Ardalis Clean Architecture template](https://github.com/ardalis/CleanArchitecture) +- [fullstackhero starter kit](https://github.com/fullstackhero/dotnet-starter-kit) +- [Finbuckle.MultiTenant documentation](https://www.finbuckle.com/MultiTenant/Docs) +- [ServiceStack documentation](https://docs.servicestack.net/why-servicestack) +- [DevExpress XAF overview](https://www.devexpress.com/products/net/application_framework/) + +Product names in this article belong to their owners. Mention is for identification in a technical comparison, not affiliation or endorsement. diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Cover.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Cover.png new file mode 100644 index 0000000000..b9e13e4e9f Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Cover.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Post.md b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Post.md new file mode 100644 index 0000000000..9517661b73 --- /dev/null +++ b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/Post.md @@ -0,0 +1,177 @@ +# How to Present Your .NET Project To Your Mother? + +**I'm writing this article while camping in the middle of the forest 🌲⛺.** I came here to get away from computers for a while... but apparently, I'm still thinking about software and how we explain it to people. You can see me in the cover image 😄 + +![image-20260906225734694](image-20260906225734694.png) + +Hey there! I'm Alper, giving international conference talks and mostly I don't know the audience demography (knowledge level/position/tech stack etc...). And I need to be very careful how I present my talk. Recently I've been studying on this topic. My audience is mostly developers or IT guys. And when I explain a technical topic to those people, I'm very comfortable 🤠🤠 But, sometimes we need to explain what we do (our project) to our friends, mother or child. In other words; anyone who is not technical. Then things are getting a little bit different and harder 🤯 + +To have a strong technical knowledge does not automatically make someone a good communicator. +For scientists, engineers, developers and technical leaders, the real challenge is not knowing the subject 👉 **deciding what the audience needs to know and how to make it meaningful for them.** + +Empathy is very important in all our life steps. I wrote an article about [empathy in workspace](https://abp.io/community/articles/empathy-in-the-workplace-for-software-companies-wsjjw9we), please read it when you have time because it's also important for this topic. Imagine you are explaining your .NET project to an investor or customer, concepts which you use in your project might be simple for you, but feel that empathy (please) for a moment, **does that guy understand what you mean**? Before starting your presentation collect information about him so that you can adjust your sentences according to him. + +> AND TELL HIM ONLY WHAT WORKS FOR HIM. + +No need to tell how the algorithm behind this project works! Tell him why it's good for him. From now on, when I tell "him/his" I mean the customer / investor / friend / father / son / non-IT guy. + + + +--- + +## Don't Focus Too Much on the Details + +We are all good software engineers and we want to show the art behind that work 👏 That's absolutely natural feeling 👍 +We all want to show people the details how our project work because we are proud of it and that's a bad practise!! +Too much detail will definitely hide your main message!!! You are consuming his energy, focus and time with all those details. +So what happens, your main goal is not being transferred to him 🤔..💭...🥺 + +![image-20260906212829582](image-20260906212829582.png) + +Ok we know the problem, now how will you fix this? SIIMMPLEE: Before mentioning a topic, ask yourself: + +> Does he need this info to understand my idea or make his decision? + +If not, please save it for yourself and don't mention about that. +Maybe later, if he's very much interested in you can explain him (IF ONLY he asks 😄) + + + +--- + +## Don't Make It Too Simple Also *-- are you kidding me!* + +Well! Don't be angry 😡 at me. In the previous section I said *don't go in details* and now I'm telling **do not explain too simple** 🙃 +While we are trying to not make it too complicated or detailed, we shouldn't make it inaccurate or treating the listener as idiot🐑 + +Keep the science and technical meaning balanced. Let's reduce unnecessary software jargons. Use clear language, examples and familiar concepts when you explain something complex. +**My technique is using analogies**, especially for my projects I often give a car example which is as simple as everybody can easily understand (*even my kid understands it*). + +> MAKE IT SIMPLE, but NOT OVER-SIMPLE! + + + +--- + +## Start With Why It Matters + +In Turkey, there are some street sellers... They stop you and say "Can I tell you something?". Actually I know he's a seller and probably he'll redirect me to a barber, cosmetic store or some shops which are upper floors in a passage. And I mostly say "NO!" and I'm walking my way... But imagine if he tells me first which benefit I'll get from what he sells, maybe I'll go to his shop. (*this paragraph is also explaining my topic in a simple way which I explained in the previous section so I'm still using these techniques in this article* 🤫) + +![image-20260906212109817](image-20260906212109817.png) + +One of the most common mistakes in technical presentations is starting with the technology itself. + +> Our project runs the Dijkstra algorithm with the multi-tenant and DDD architecture 🧑‍🔬 + +The above sentence doesn't mean anything to him. Tell it like this: + +> Our project finds the shortest path efficiently, you can have many isolated customers with clean design and long years maintainable way.” + +When people understand why your project matters, they'll have a reason to listen the next sections. + +So far, so good 💯 You did a good job reading until here 🙏 ᵗʰᵃᶰᵏᵧₒᵤ ... Keep reading please ...👀...👇 + + +--- + +## Think About What You Want Him to Remember + +Your audience will not remember many technical points after your presentation. +**They will remember 1-2 topics.** +Decide what those points are 🎯 then build the presentation around them. +Everything else must support those core messages. + + + + +--- + +## Your Voice Tone Matters Too! + +Imagine you did everything 👏 very good until here... but you are talking very monotonous way. Or you are talking quickly. Sorry, that's also not a good talk! **Your emphasis, eye contact, body language, tone of voice are all important.** You can make small jokes to give him a relaxing break to understand your complex technical work. + +> COMMUNICATION IS NOT ONLY ABOUT WHAT YOU SAY but also HOW YOU SAY IT 🗣 + +Watch the📹 [ShadeZahrai -very short- tiktok video](https://www.tiktok.com/@shadezahrai/video/7177549216464571650) + +![image-20260906210745253](image-20260906210745253.png) + + +--- + +## Use Images to Make Your Subject Clear + +Especially when you are presenting on slides, you should show some funny images related to your topic or you can show analogic images that explains the problem. +For example to tell about a risky case which you covered in your project, you can use the below image :) +Yes, people will laugh at first but later they'll listen to you more carefully because you took their attention. (*I created another paradox here, I’m explaining this topic using a funny related image and I know you’re now reading my article more carefully*🤝) + +![image-20260906201600152](image-20260906201600152.png) + + +--- + + + +## How to Handle Questions? 😡 Especially Which You Don't Like + +![image-20260906202807792](image-20260906202807792.png) + +In technical presentations, people can ask aggressive or stupid questions or you may not like their questions 😡 +Don't immediately try to defend yourself by giving more and more technical details. Or don't try to show them it's a silly question. +If you don't know, tell it honestly. If that feature doesn't exist, tell that you took note and you'll evaluate it later. +Even if it’s a stupid question, don’t shut him down. **Later, he will remember only that moment and all your efforts you’ve made will be lost.** + + + + +--- + +## Your Goal Is To Be Understandable 🫱🏽‍🫲🏻 Not Showing Everything You Know + +Anddd again, we come back to empathy! + +> The best technical explainer is not the person who gives the most information. + +![image-20260906204302915](image-20260906204302915-1788717035148-1.png) + +Okay, you know a lot and you want to show that. But try to be empathetic again. Giving someone all the technical details doesn't mean they will understand your point. **Good communication means sharing what matters in a way people can easily understand.** + +> Don't tell people everything you know. Tell them what they need to know and please make sure they understand it. + + + +--- + +## Explaining Something and Convincing Someone Are Not the Same 🙄 + +Providing accurate & correct information and persuading someone to take action are different. +But in business life, **technical presentations often need to do both**. +You need to explain how something works and also helping management approve a project. + +YOU CAN USE THIS PATH WHEN PRESENTING YOUR PROJECT: + +![image-20260906203343446](image-20260906203343446.png) + +--- + +I appreciate you taking the time to read this article 🙏. +I like to share what I learn which is the main reason I wrote this article. +By applying these techniques, we can become better presenters and communicate our ideas more effectively 👌. +When we combine our technical strengths with strong soft skills, I believe we can make a greater impact, inspire others and grow together 🙌💪. + + + +--- + +Alper Ebicoglu + +Software architect since the early 2000s. +Learning, inspiring, sharing, talking... + +||| +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| | [linkedin.com/in/ebicoglu](https://www.linkedin.com/in/ebicoglu/) | +| | [x.com/alperebicoglu](https://x.com/alperebicoglu) | +| | [alperonline.medium.com](https://alperonline.medium.com) | +| | [github.com/ebicoglu](https://github.com/ebicoglu) | + diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906201600152.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906201600152.png new file mode 100644 index 0000000000..7cc2c3bddc Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906201600152.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906202807792.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906202807792.png new file mode 100644 index 0000000000..5e9b0c352b Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906202807792.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906203343446.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906203343446.png new file mode 100644 index 0000000000..581d8b3967 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906203343446.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906204302915-1788717035148-1.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906204302915-1788717035148-1.png new file mode 100644 index 0000000000..d05bfebe6b Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906204302915-1788717035148-1.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906210745253.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906210745253.png new file mode 100644 index 0000000000..db5d1ef512 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906210745253.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212109817.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212109817.png new file mode 100644 index 0000000000..d993848029 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212109817.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212829582.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212829582.png new file mode 100644 index 0000000000..45340db0bd Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906212829582.png differ diff --git a/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906225734694.png b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906225734694.png new file mode 100644 index 0000000000..ae2a31cec5 Binary files /dev/null and b/docs/en/Community-Articles/2026-09-06-How-to-Present-Your-Project-To-Your-Mother/image-20260906225734694.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 0b79368bae..0d1ab82103 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -2589,7 +2589,7 @@ "path": "solution-templates/microservice/helm-charts-and-kubernetes.md" }, { - "text": ".NET Aspire Integration", + "text": "Aspire Integration", "path": "solution-templates/microservice/aspire-integration.md" }, { diff --git a/docs/en/framework/architecture/domain-driven-design/application-services.md b/docs/en/framework/architecture/domain-driven-design/application-services.md index 8683241534..a2d2db5acc 100644 --- a/docs/en/framework/architecture/domain-driven-design/application-services.md +++ b/docs/en/framework/architecture/domain-driven-design/application-services.md @@ -444,6 +444,7 @@ These methods are low level methods that can control how to query entities from * `ApplyPaging` is used to make paging on the query. If your `TGetListInput` already implements `IPagedResultRequest`, you don't need to override this since the ABP automatically understands it and performs the paging. * `ApplySorting` is used to sort (order by...) the query. If your `TGetListInput` already implements the `ISortedResultRequest`, ABP automatically sorts the query. If not, it fallbacks to the `ApplyDefaultSorting` which tries to sort by creation time, if your entity implements the standard `IHasCreationTime` interface. * `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default. +* `CreateEntityQueryOrNullAsync` is used to create a query for a single entity by id, which is only needed for the *Query Projection* explained below. It returns `null` if the application service can not create such a query, then `GetEntityByIdAsync` is used. * `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default. #### Object to Object Mapping @@ -456,6 +457,103 @@ These methods are used to convert Entities to DTOs and vice verse. They use the * `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`. * `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`. +#### Query Projection + +`GetAsync` and `GetListAsync` get the entities from the database, then map them to DTOs in the memory. If your DTO uses only a few properties of a large entity, you can project the query to the DTO instead, so the database returns only the columns you need. + +Implement the `IQueryProjector` interface to define a projection: + +````csharp +using System.Linq; +using Volo.Abp.ObjectMapping; + +namespace MyProject.Books; + +public class BookProjector : IQueryProjector +{ + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(book => new BookDto + { + Id = book.Id, + Name = book.Name + }); + } +} +```` + +You don't have to write the `Select` by hand. Both [Mapperly](https://mapperly.riok.app/) and [AutoMapper](https://docs.automapper.org) can project an `IQueryable`, refer to their own documentation for it and to the [object to object mapping document](../../infrastructure/object-to-object-mapping.md) for their ABP integrations. Your existing maps are not used for the projection, a projector is always a class implementing `IQueryProjector`. + +ABP registers the projectors by convention, you don't need to configure anything else. Implement a projector once for an entity and DTO pair, and use the `ReplaceServices` option of the `DependencyAttribute` to replace an existing one. Filters (like soft delete and multi-tenancy), sorting and paging are still applied to the query before the projection. + +> A projection must return one row per entity. The total count and the paging are calculated on the entity query before the projection runs, so a projection that filters out rows (an inner join to an optional relation) or multiplies them (a join to a collection) returns a page that doesn't match the reported total count. Use a left join for optional relations. + +The projector is synchronous, so it can not obtain the query of another aggregate root, which is only +available through the asynchronous `GetQueryableAsync`. Override `CreateGetOutputDtoQueryOrNullAsync` or +`CreateGetListOutputDtoQueryOrNullAsync` for that. They replace the projector for that application service: + +````csharp +public class BookAppService : ReadOnlyAppService +{ + private readonly IBookDtoQuery _bookDtoQuery; + + //... + + protected override async Task?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable query) + { + return await _bookDtoQuery.ProjectAsync(query); + } +} + +//The projection is a class of its own, so the other application services returning a BookDto reuse it +public class BookDtoQuery : IBookDtoQuery, ITransientDependency +{ + private readonly IReadOnlyRepository _authorRepository; + + //... + + public async Task> ProjectAsync(IQueryable books) + { + var authors = await _authorRepository.GetQueryableAsync(); + + return from book in books + join author in authors on book.AuthorId equals author.Id into bookAuthors + from bookAuthor in bookAuthors.DefaultIfEmpty() + select new BookDto + { + Id = book.Id, + Name = book.Name, + AuthorName = bookAuthor != null ? bookAuthor.Name : null + }; + } +} +```` + +Both queries must come from the same database context, otherwise they can not be executed as a single query, +and the provider has to be able to translate the join. The one row per entity rule above applies here too, +that's why the example uses a left join. A joined column can not be used for the sorting, and the paging is +based on the entity query, since both are applied before this method is called. + +A projector is resolved by the `(entity, DTO)` type pair, just like an `IObjectMapper`, so registering one enables the projection for every application service using that pair. It replaces the way the DTOs are read: + +* `GetListAsync` doesn't use `MapToGetListOutputDtosAsync` anymore. +* `GetAsync` doesn't use `GetEntityByIdAsync` and `MapToGetOutputDtoAsync` anymore, as long as the application service can create a query for a single entity. `ReadOnlyAppService` and `CrudAppService` already do that. A class deriving from `AbstractKeyReadOnlyAppService` has to override `CreateEntityQueryOrNullAsync`, otherwise `GetAsync` keeps loading the entity and mapping it. + +The rest of the pipeline is untouched. The authorization policies are still checked, `CreateFilteredQueryAsync`, `ApplySorting` and `ApplyPaging` are still used, the data filters (like soft delete and multi-tenancy) are still applied, and the create, update and delete methods still use the [IObjectMapper](../../infrastructure/object-to-object-mapping.md). + +> If an application service needs to keep using the entity based extension points, override the `GetOutputDtoQueryProjector` or `GetListOutputDtoQueryProjector` property and return `null`: + +````csharp +public class BookAppService : CrudAppService +{ + protected override IQueryProjector? GetOutputDtoQueryProjector => null; + + protected override IQueryProjector? GetListOutputDtoQueryProjector => null; + + //... +} +```` + ## Miscellaneous ### Working with Streams diff --git a/docs/en/framework/fundamentals/fluent-validation.md b/docs/en/framework/fundamentals/fluent-validation.md index 8270e65b72..7a73396ab8 100644 --- a/docs/en/framework/fundamentals/fluent-validation.md +++ b/docs/en/framework/fundamentals/fluent-validation.md @@ -60,6 +60,74 @@ public class CreateUpdateBookDtoValidator : AbstractValidator A `Range` attribute that writes its limits as strings, like `[Range(typeof(decimal), "1.5", "9.5")]`, reads them in the culture of the request unless it sets `ParseLimitsInInvariantCulture`. Set it, so that the limit means the same thing to the server and to the api definition on every request. + +When a rule and an attribute constrain the same property, the stricter bound is used: the higher minimum and the lower maximum. When both bounds have the same value, the exclusive one is used. The exclusivity always comes from the bound that is used, so `[Range(0, 100)]` with `GreaterThan(-5)` results in an inclusive `Minimum = 0`. A non-numeric bound, like a `Range` attribute on a `DateTime` property, is kept as-is. An existing `Regex` is also kept, because a single value can not express two patterns that both have to match. + +### Rules That Are Not Mapped + +The following rules are not mapped, because they don't apply to every instance of the DTO: + +* Rules under `When(...)` / `Unless(...)` (both the chained and the block form) and their async variants, because the same property can be required for one instance and optional for another. +* Rules that only belong to a non-default rule set, because ABP validates with FluentValidation's default selector, which does not run them. +* `RuleForEach(...)` rules, because they constrain the items of a collection rather than the collection property. +* Comparisons on a property that is not a number, and comparisons against another property. `Minimum` and `Maximum` are numeric bounds, so the ordinal comparison of two strings can not be published there. + +### Rules That Are Not Fully Expressed + +The following rules are not fully expressed in the API definition: + +* A zero length bound, from `MaximumLength(0)` or `Length(0, 0)`, is not published. Every length rule has a `Func` form that reports the same zero on the descriptor, so the two can not be told apart. +* Rules that come from an `Include(...)` call are not published, because FluentValidation does not expose the included validator on its descriptor. +* A validator of a derived DTO can not add rules to a property declared by its base class, because each type describes only its own properties. +* A rule on a nested object, like `RuleFor(x => x.Address.City)`, is not published either. The nested type is described on its own, with its own validator, and its model is shared by every DTO that uses it. +* A validator of a closed generic DTO is not used, because the API definition describes the generic type definition, which is shared by all of its instantiations. +* `InclusiveBetween(...)` and `ExclusiveBetween(...)` with their own `IComparer` are only published when their bounds still read as an interval in the natural order. FluentValidation does not expose the comparer, so a rule that orders its values differently can not be recognised. +* `Matches(pattern, RegexOptions)` publishes the pattern without the options. This is the one case where a client can be stricter than the server, so avoid the overload if the client should not reject what the server accepts. + +> The API definition describes a type, while the server runs the validation per action. So, a DTO that is only used as a return value, or that is sent to an action which doesn't validate its parameters, still declares its constraints here. This is also how the data annotation attributes have always been reported. + ## See Also * [Validation System](./validation.md) \ No newline at end of file diff --git a/docs/en/framework/ui/mvc-razor-pages/bundling-minification.md b/docs/en/framework/ui/mvc-razor-pages/bundling-minification.md index 0114361188..5e6078c155 100644 --- a/docs/en/framework/ui/mvc-razor-pages/bundling-minification.md +++ b/docs/en/framework/ui/mvc-razor-pages/bundling-minification.md @@ -35,7 +35,7 @@ The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-styl - + ```` @@ -65,7 +65,7 @@ The `name` is **optional** for the razor bundle tag helpers. If you don't define - + @if (ViewBag.IncludeCustomStyles != false) { @@ -115,7 +115,7 @@ public class MyWebModule : AbpModule bundle.AddFiles( "/libs/jquery/jquery.js", "/libs/bootstrap/js/bootstrap.js", - "/libs/toastr/toastr.min.js", + "/libs/lodash/lodash.min.js", "/scripts/my-global-scripts.js" ); }); @@ -220,7 +220,7 @@ Contributors can also be used in the bundle tag helpers. Example: - + ```` @@ -386,7 +386,7 @@ Configure(options => ````html - + ```` ### External/CDN file Support diff --git a/docs/en/framework/ui/mvc-razor-pages/overall.md b/docs/en/framework/ui/mvc-razor-pages/overall.md index b029fda57c..5e0fc41435 100644 --- a/docs/en/framework/ui/mvc-razor-pages/overall.md +++ b/docs/en/framework/ui/mvc-razor-pages/overall.md @@ -57,14 +57,11 @@ There are a set of standard JavaScript/CSS libraries that comes pre-installed an - [JQuery Validation](https://jqueryvalidation.org/) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation - [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. - [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. -- [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. - [Lodash](https://lodash.com/) as a utility library. - [Luxon](https://moment.github.io/luxon/) for date/time operations. -- [JQuery Form](https://github.com/jquery-form/form) for AJAX forms. - [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers. - [Select2](https://select2.org/) for better select/combo boxes. -- [Timeago](http://timeago.yarp.com/) to show automatically updating fuzzy timestamps. -- [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars. +- [timeago.js](https://timeago.org/) to show automatically updating fuzzy timestamps. You can use these libraries directly in your applications, without needing to manually import your page. @@ -140,7 +137,7 @@ ABP provides a flexible and modular Bundling & Minification system to create bun - + ``` diff --git a/docs/en/framework/ui/mvc-razor-pages/theming.md b/docs/en/framework/ui/mvc-razor-pages/theming.md index 138b52e3d2..dafbfcb2d3 100644 --- a/docs/en/framework/ui/mvc-razor-pages/theming.md +++ b/docs/en/framework/ui/mvc-razor-pages/theming.md @@ -44,14 +44,11 @@ All the themes must depend on the [@abp/aspnetcore.mvc.ui.theme.shared](https:// * [JQuery Validation](https://github.com/jquery-validation/jquery-validation) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. * [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. -* [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. * [Lodash](https://lodash.com/) as a utility library. * [Luxon](https://moment.github.io/luxon/) for date/time operations. -* [JQuery Form](https://github.com/jquery-form/form) for AJAX forms. * [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers. * [Select2](https://select2.org/) for better select/combo boxes. -* [Timeago](http://timeago.yarp.com/) to show automatically updating fuzzy timestamps. -* [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars. +* [timeago.js](https://timeago.org/) to show automatically updating fuzzy timestamps. These libraries are selected as the base libraries and available to the applications and modules. @@ -60,7 +57,7 @@ These libraries are selected as the base libraries and available to the applicat There are some abstractions in the ABP to make your code independent from some of these libraries too. Examples; * [Tag Helpers](tag-helpers) makes it easy to generate the Bootstrap UIs. -* JavaScript [Message](javascript-api/message.md) and [Notification](javascript-api/notify.md) APIs provides abstractions to use the Sweetalert and Toastr. +* JavaScript [Message](javascript-api/message.md) and [Notification](javascript-api/notify.md) APIs provide abstractions to use the SweetAlert and the built-in toast implementations. * [Forms & Validation](forms-validation.md) system automatically handles the validation, so you mostly don't directly type any validation code. ### The Standard Layouts diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index ec2d4eb513..41d61647f2 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -110,11 +110,11 @@ In this step, you can choose which languages your application will support. * Click Add Custom Language if you want to add a language that is not listed. -Click the Next button to see *.NET Aspire* configuration selection: +Click the Next button to see *Aspire* configuration selection: ![abp-studio-new-solution-dialog-aspire-configuration](images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png) -In this step, you can enable or disable the .NET Aspire integration for your solution. If you enable it, the solution will be pre-configured to work with .NET Aspire for easier microservice development and deployment. See the [Aspire Integration](../solution-templates/microservice/aspire-integration.md) document for more information about this feature. +In this step, you can enable or disable the Aspire integration for your solution. If you enable it, the solution will be pre-configured to work with Aspire for easier microservice development and deployment. See the [Aspire Integration](../solution-templates/microservice/aspire-integration.md) document for more information about this feature. Click the Next button to see *Additional Options* selection: diff --git a/docs/en/solution-templates/microservice/aspire-integration.md b/docs/en/solution-templates/microservice/aspire-integration.md index 2da2706b34..33f9c80a23 100644 --- a/docs/en/solution-templates/microservice/aspire-integration.md +++ b/docs/en/solution-templates/microservice/aspire-integration.md @@ -1,11 +1,11 @@ ```json //[doc-seo] { - "Description": "Learn how to integrate .NET Aspire into your ABP-based solution for streamlined development, orchestration, and observability of distributed applications." + "Description": "Learn how to integrate Aspire into your ABP-based solution for streamlined development, orchestration, and observability of distributed applications." } ``` -# .NET Aspire Integration +# Aspire Integration ````json //[doc-nav] @@ -19,7 +19,7 @@ > You must have an ABP Business or a higher license to be able to create a microservice solution. -## .NET Aspire Overview +## Aspire Overview [Aspire](https://aspire.dev/get-started/what-is-aspire/) streamlines building, running, debugging, and deploying distributed apps. Picture your app as a set of services, databases, and frontends—when they’re deployed, they all work together seamlessly, but every time you develop them they need to be individually started and connected. With Aspire, you get a unified toolchain that eliminates complex configs and makes local debugging effortless. Instantly launch and debug your entire app with a single command. Ready to deploy? Aspire lets you publish anywhere—Kubernetes, the cloud, or your own servers. It’s also fully extensible, so you can integrate your favorite tools and services with ease. It provides: @@ -31,7 +31,7 @@ ## ABP Integration -When you enable .NET Aspire in an ABP microservice solution, you get a fully integrated development experience where: +When you enable Aspire in an ABP microservice solution, you get a fully integrated development experience where: - All microservices, gateways, and applications are orchestrated through a single entry point (AppHost). - Infrastructure containers (databases, Redis, RabbitMQ, Elasticsearch, etc.) are managed as code. @@ -41,8 +41,8 @@ When you enable .NET Aspire in an ABP microservice solution, you get a fully int When creating a new microservice solution via ABP Studio: -1. In the solution creation wizard, look for the **".NET Aspire Integration"** step. -2. Toggle the option to **enable .NET Aspire**. +1. In the solution creation wizard, look for the **"Aspire Integration"** step. +2. Toggle the option to **enable Aspire**. 3. Complete the wizard—Aspire projects will be generated along with your solution. ![Enable Aspire in ABP Studio](images/aspire-configuration.png) @@ -55,7 +55,7 @@ When Aspire is enabled, two additional projects are added to your solution: ### AppHost (Orchestrator) -[`AppHost`](https://aspire.dev/get-started/app-host/) is the .NET Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. All services, gateways, and applications in the solution have their project references added to `AppHost`. +[`AppHost`](https://aspire.dev/get-started/app-host/) is the Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. All services, gateways, and applications in the solution have their project references added to `AppHost`. **Why is it added?** @@ -367,6 +367,6 @@ To run your resource in **Solution Runner** profiles(Default or Aspire), add it ## Deploying the Application -.NET Aspire supports deployment to Azure Container Apps, Kubernetes, and other cloud platforms. For detailed deployment guidance, see the official documentation: [.NET Aspire Deployment](https://aspire.dev/deployment/overview/) +Aspire supports deployment to Azure Container Apps, Kubernetes, and other cloud platforms. For detailed deployment guidance, see the official documentation: [Aspire Deployment](https://aspire.dev/deployment/overview/) -To learn more about .NET Aspire, visit: https://aspire.dev/get-started/what-is-aspire/ +To learn more about Aspire, visit: https://aspire.dev/get-started/what-is-aspire/ diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index 7778860259..a23501ba62 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -262,7 +262,7 @@ This document contains **brief release notes** for each ABP Studio release. Rele * Added `AI Management` option to Startup Templates (app-nolayers, app). * Added support for new `SLNX` solution file format. * Enhanced modularity step in solution creation process. -* Fixed Swagger authorization issues when projects run via .NET Aspire. +* Fixed Swagger authorization issues when projects run via Aspire. * Fixed browser notification overlay problems. * Added missing `Unit of Work` namespace in solution templates. * Fixed JSON file formatting issues. @@ -355,7 +355,7 @@ This document contains **brief release notes** for each ABP Studio release. Rele ## 1.3.0 (2025-09-22) * Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.3.4`) -* **Added .NET Aspire Integration** to ABP Studio and Microservice Startup Template. +* **Added Aspire Integration** to ABP Studio and Microservice Startup Template. * **Introduced AI Support Assistant** for enhanced development experience. * Added new package option: **C# Console Application (With ABP)**. * Enhanced Solution Runner with double-click browse functionality. diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 6b9ad8af61..cc0a1dcf8e 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -121,6 +121,7 @@ + @@ -229,6 +230,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/ko.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/ko.json new file mode 100644 index 0000000000..e004a8cff1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/ko.json @@ -0,0 +1,13 @@ +{ + "culture": "ko", + "texts": { + "GivenTenantIsNotExist": "지정된 테넌트가 존재하지 않습니다: {0}", + "GivenTenantIsNotAvailable": "지정된 테넌트를 사용할 수 없습니다: {0}", + "Tenant": "테넌트", + "Switch": "전환", + "Name": "이름", + "SwitchTenantHint": "호스트 측으로 전환하려면 이름 필드를 비워 두세요.", + "SwitchTenant": "테넌트 전환", + "NotSelected": "선택되지 않음" + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs index 7ba8105d43..0c4bab0b04 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs @@ -2,7 +2,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker; using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Moment; -using Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago; using Volo.Abp.Localization; using Volo.Abp.Modularity; @@ -38,11 +37,6 @@ public class AbpAspNetCoreMvcUiPackagesModule : AbpModule new NameValue("zh-Hant", "zh-tw"), new NameValue("de-DE", "de")); - //Timeago - options.AddLanguageFilesMapOrUpdate(TimeagoScriptContributor.PackageName, - new NameValue("zh-Hans", "zh-CN"), - new NameValue("zh-Hant", "zh-TW")); - //JQueryValidation options.AddLanguageFilesMapOrUpdate(JQueryValidationScriptContributor.PackageName, new NameValue("zh-Hans", "zh"), diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs index dac2c051aa..eef9f3478e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/BootstrapDatepicker/BootstrapDatepickerScriptContributor.cs @@ -14,7 +14,7 @@ public class BootstrapDatepickerScriptContributor : BundleContributor public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddIfNotContains("/libs/bootstrap-datepicker/bootstrap-datepicker.min.js"); + context.Files.AddIfNotContains("/libs/bootstrap-datepicker/bootstrap-datepicker.js"); } public override void ConfigureDynamicResources(BundleConfigurationContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginScriptBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginScriptBundleContributor.cs deleted file mode 100644 index 30639e1583..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginScriptBundleContributor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; - -public class MalihuCustomScrollbarPluginScriptBundleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js"); - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginStyleBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginStyleBundleContributor.cs deleted file mode 100644 index d24ccb9f77..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/MalihuCustomScrollbar/MalihuCustomScrollbarPluginStyleBundleContributor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; - -public class MalihuCustomScrollbarPluginStyleBundleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.css"); - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselScriptContributor.cs deleted file mode 100644 index 41172a9539..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselScriptContributor.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; -using Volo.Abp.Modularity; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.OwlCarousel; - -[DependsOn(typeof(JQueryScriptContributor))] -public class OwlCarouselScriptContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - context.Files.AddIfNotContains("/libs/owl.carousel/owl.carousel.min.js"); - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselStyleContributor.cs deleted file mode 100644 index b0a5ce8014..0000000000 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/OwlCarousel/OwlCarouselStyleContributor.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Volo.Abp.AspNetCore.Mvc.UI.Bundling; - -namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.OwlCarousel; - -public class OwlCarouselStyleContributor : BundleContributor -{ - public override void ConfigureBundle(BundleConfigurationContext context) - { - //TODO: Theming! - context.Files.AddIfNotContains("/libs/owl.carousel/assets/owl.carousel.min.css"); - context.Files.AddIfNotContains("/libs/owl.carousel/assets/owl.theme.default.min.css"); - context.Files.AddIfNotContains("/libs/owl.carousel/assets/owl.theme.green.min.css"); - } -} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperScriptContributor.cs new file mode 100644 index 0000000000..2ead1c6ced --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperScriptContributor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Swiper; + +public class SwiperScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/swiper/swiper-bundle.min.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperStyleContributor.cs new file mode 100644 index 0000000000..6fd744dcb7 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Swiper/SwiperStyleContributor.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Swiper; + +public class SwiperStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/swiper/swiper-bundle.min.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs index 3b09dd1022..94fbd9a3b9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Timeago/TimeagoScriptContributor.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; -using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; -using Volo.Abp.Localization; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago; @@ -10,20 +8,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago; [DependsOn(typeof(JQueryScriptContributor))] public class TimeagoScriptContributor : BundleContributor { - public const string PackageName = "jquery.timeago"; - public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddIfNotContains("/libs/timeago/jquery.timeago.js"); - } - - public override void ConfigureDynamicResources(BundleConfigurationContext context) - { - var fileName = context.LazyServiceProvider.LazyGetRequiredService>().Value.GetCurrentUICultureLanguageFilesMap(PackageName); - var filePath = $"/libs/timeago/locales/jquery.timeago.{fileName}.js"; - if (context.FileProvider.GetFileInfo(filePath).Exists) - { - context.Files.Add(filePath); - } + context.Files.AddIfNotContains("/libs/timeago/timeago.full.min.js"); + context.Files.AddIfNotContains("/libs/abp/timeago/abp.timeago.js"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs index 728becdf6d..7865ec9c3d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs @@ -7,7 +7,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidationUnobtrusive; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Lodash; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Luxon; -using Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2; using Volo.Abp.AspNetCore.Mvc.UI.Packages.SweetAlert2; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago; @@ -23,7 +22,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; typeof(Select2ScriptContributor), typeof(DatatablesNetBs5ScriptContributor), typeof(Sweetalert2ScriptContributor), - typeof(MalihuCustomScrollbarPluginScriptBundleContributor), typeof(LuxonScriptContributor), typeof(TimeagoScriptContributor), typeof(BootstrapDatepickerScriptContributor), diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs index 4f436a02b0..b030508711 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs @@ -5,7 +5,6 @@ using Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDaterangepicker; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core; using Volo.Abp.AspNetCore.Mvc.UI.Packages.DatatablesNetBs5; using Volo.Abp.AspNetCore.Mvc.UI.Packages.FontAwesome; -using Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2; using Volo.Abp.Modularity; @@ -16,7 +15,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; typeof(BootstrapStyleContributor), typeof(FontAwesomeStyleContributor), typeof(Select2StyleContributor), - typeof(MalihuCustomScrollbarPluginStyleBundleContributor), typeof(DatatablesNetBs5StyleContributor), typeof(BootstrapDatepickerStyleContributor), typeof(BootstrapDaterangepickerStyleContributor) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js index 3199900536..cb1b38ab8c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js @@ -72,7 +72,7 @@ } abp.dom.initializers.initializeTimeAgos = function ($timeagos) { - $timeagos.timeago(); + abp.timeago.render($timeagos.toArray()); } abp.dom.initializers.initializeAutocompleteSelects = function ($autocompleteSelects) { @@ -217,6 +217,7 @@ args.$el.findWithSelf('[data-bs-toggle="tooltip"]').each(function () { $('#' + $(this).attr('aria-describedby')).remove(); }); + abp.timeago.cancel(args.$el.findWithSelf('[timeago-id]').toArray()); }); abp.event.on('abp.configurationInitialized', function () { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index bd5e1e82a0..837eb929fe 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -34,19 +34,22 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide private readonly AbpAspNetCoreMvcOptions _abpAspNetCoreMvcOptions; private readonly AbpApiDescriptionModelOptions _modelOptions; private readonly IXmlDocumentationProvider _xmlDocProvider; + private readonly IPropertyApiDescriptionModelContributor[] _propertyContributors; public AspNetCoreApiDescriptionModelProvider( IOptions options, IApiDescriptionGroupCollectionProvider descriptionProvider, IOptions abpAspNetCoreMvcOptions, IOptions modelOptions, - IXmlDocumentationProvider xmlDocProvider) + IXmlDocumentationProvider xmlDocProvider, + IEnumerable propertyContributors) { _options = options.Value; _descriptionProvider = descriptionProvider; _abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value; _modelOptions = modelOptions.Value; _xmlDocProvider = xmlDocProvider; + _propertyContributors = propertyContributors.ToArray(); Logger = NullLogger.Instance; } @@ -321,6 +324,8 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide await PopulateTypeDescriptionsAsync(applicationModel.Types[typeName], type); } + await ContributeToPropertiesAsync(applicationModel.Types[typeName], type); + await AddCustomTypesToModelAsync(applicationModel, type.BaseType, includeDescriptions); foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type)) @@ -329,6 +334,33 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide } } + protected virtual async Task ContributeToPropertiesAsync(TypeApiDescriptionModel typeModel, Type type) + { + if (_propertyContributors.IsNullOrEmpty() || typeModel.Properties.IsNullOrEmpty()) + { + return; + } + + var propertyInfos = type + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.DeclaringType == type) + .ToDictionary(p => p.Name, p => p); + + foreach (var propertyModel in typeModel.Properties!) + { + if (!propertyInfos.TryGetValue(propertyModel.Name, out var propertyInfo)) + { + continue; + } + + var context = new PropertyApiDescriptionModelContributionContext(propertyModel, propertyInfo, type); + foreach (var contributor in _propertyContributors) + { + await contributor.ContributeAsync(context); + } + } + } + private static string CalculateTypeName(Type type) { if (!type.IsGenericTypeDefinition) diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Localization/ko.json b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Localization/ko.json new file mode 100644 index 0000000000..a41eae4912 --- /dev/null +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Localization/ko.json @@ -0,0 +1,10 @@ +{ + "culture": "ko", + "texts": { + "Volo.Authorization:010001": "권한 부여에 실패했습니다! 지정된 정책에서 권한이 부여되지 않았습니다.", + "Volo.Authorization:010002": "권한 부여에 실패했습니다! 지정된 정책에서 권한이 부여되지 않았습니다: {PolicyName}", + "Volo.Authorization:010003": "권한 부여에 실패했습니다! 지정된 리소스에 대해 정책에서 권한이 부여되지 않았습니다: {ResourceName}", + "Volo.Authorization:010004": "권한 부여에 실패했습니다! 지정된 리소스에 대해 요구 사항이 충족되지 않았습니다: {ResourceName}", + "Volo.Authorization:010005": "권한 부여에 실패했습니다! 지정된 리소스에 대해 요구 사항들이 충족되지 않았습니다: {ResourceName}" + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs index b7dca40bad..3f1651eb77 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs @@ -32,6 +32,8 @@ public class SwitchToNightlyCommand : IConsoleCommand, ITransientDependency sb.AppendLine(""); sb.AppendLine("Options:"); sb.AppendLine("-d|--directory"); + sb.AppendLine("-i|--include (optional) comma-separated list of Directory.Packages.props-style files to also update for Central Package Management"); + sb.AppendLine("-ep|--exclude-packages (optional) comma-separated list of package ids to never touch in --include files"); sb.AppendLine(""); sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli"); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 9b7340fdd7..379fc45bfa 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -132,7 +132,7 @@ public class NpmPackagesUpdater : ITransientDependency if (!fileContent.Contains(volosoftRegistry)) { - fileContent += volosoftRegistry; + fileContent += Environment.NewLine + volosoftRegistry; } File.WriteAllText(fileName, fileContent); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs index 13046dae78..9dfacf5431 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -66,13 +67,13 @@ public class PackagePreviewSwitcher : ITransientDependency if (solutionPaths.Any()) { - await SwitchSolutionsToNightlyPreview(solutionPaths); + await SwitchSolutionsToNightlyPreview(solutionPaths, commandLineArgs); } else { var projectPaths = GetProjectPaths(commandLineArgs); - - await SwitchProjectsToNightlyPreview(projectPaths); + + await SwitchProjectsToNightlyPreview(projectPaths, commandLineArgs); } } @@ -185,13 +186,16 @@ public class PackagePreviewSwitcher : ITransientDependency } } - private async Task SwitchProjectsToNightlyPreview(List projects) + private async Task SwitchProjectsToNightlyPreview(List projects, CommandLineArgs commandLineArgs) { + var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs); + foreach (var project in projects) { var folder = Path.GetDirectoryName(project); + var projectFolder = FindSolutionFolder(project) ?? folder; - _packageSourceManager.Add(FindSolutionFolder(project) ?? folder, "ABP Nightly", + _packageSourceManager.Add(projectFolder, "ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json", "Volo.*"); await _nugetPackagesVersionUpdater.UpdateSolutionAsync( @@ -201,11 +205,17 @@ public class PackagePreviewSwitcher : ITransientDependency await _npmPackagesUpdater.Update( folder, true); + + // See SwitchSolutionsToNightlyPreview for the race-avoidance rationale: this + // sequential pass always runs after the per-project UpdateSolutionAsync above. + await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, projectFolder); } } - private async Task SwitchSolutionsToNightlyPreview(List solutionPaths) + private async Task SwitchSolutionsToNightlyPreview(List solutionPaths, CommandLineArgs commandLineArgs) { + var (includeFiles, excludedPackages, latestVersionFromMyGet) = await ResolveNightlyIncludeContextAsync(commandLineArgs); + foreach (var solutionPath in solutionPaths) { var solutionFolder = Path.GetDirectoryName(solutionPath); @@ -232,6 +242,67 @@ public class PackagePreviewSwitcher : ITransientDependency solutionAngularFolder, true); } + + // Optional Central Package Management support: only runs when --include is + // explicitly passed, and only after UpdateSolutionAsync's internal parallel + // (Task.WaitAll) per-project update has fully completed, so no --include file + // is ever touched concurrently with anything else. + await UpdateIncludedCentralPackageFilesAsync(includeFiles, excludedPackages, latestVersionFromMyGet, solutionFolder); + } + } + + private async Task<(List IncludeFiles, List ExcludedPackages, string LatestVersionFromMyGet)> ResolveNightlyIncludeContextAsync( + CommandLineArgs commandLineArgs) + { + var includeFiles = GetCommaSeparatedOption(commandLineArgs, Options.Include.Short, Options.Include.Long); + var excludedPackages = GetCommaSeparatedOption(commandLineArgs, Options.Exclude.Short, Options.Exclude.Long); + + if (!includeFiles.Any()) + { + return (includeFiles, excludedPackages, null); + } + + string latestVersionFromMyGet; + try + { + latestVersionFromMyGet = await _nugetPackagesVersionUpdater.GetLatestVersionFromMyGet("Volo.Abp.Core"); + } + catch (Exception ex) + { + // Don't let a transient MyGet failure abort the whole switch-to-nightly run + // (source registration / regular PackageReference updates below must still + // proceed for every solution/project) - just skip the --include pass. + Logger.LogWarning(ex, "Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run."); + return (includeFiles, excludedPackages, null); + } + + if (latestVersionFromMyGet.IsNullOrWhiteSpace()) + { + // No exception was thrown, but MyGet simply has no version for this package yet + // (e.g. not indexed there) - warn so users aren't left wondering why --include did nothing. + Logger.LogWarning("Could not resolve the latest Volo.Abp.Core nightly version; --include files will be skipped for this run."); + return (includeFiles, excludedPackages, null); + } + + return (includeFiles, excludedPackages, latestVersionFromMyGet); + } + + private async Task UpdateIncludedCentralPackageFilesAsync( + List includeFiles, + List excludedPackages, + string latestVersionFromMyGet, + string baseFolder) + { + foreach (var includeFile in includeFiles) + { + var resolvedPath = Path.IsPathRooted(includeFile) + ? includeFile + : Path.Combine(baseFolder, includeFile); + + await _nugetPackagesVersionUpdater.UpdateCentralPackageVersionsAsync( + resolvedPath, + latestVersionFromMyGet, + excludedPackages); } } @@ -285,6 +356,14 @@ public class PackagePreviewSwitcher : ITransientDependency ?? Directory.GetCurrentDirectory(); } + private List GetCommaSeparatedOption(CommandLineArgs commandLineArgs, string shortName, string longName) + { + var raw = commandLineArgs.Options.GetOrNull(shortName, longName); + return raw.IsNullOrWhiteSpace() + ? new List() + : raw.Split(',').Select(s => s.Trim()).Where(s => !s.IsNullOrWhiteSpace()).ToList(); + } + private string GetSolutionAngularFolder(string solutionFolder) { var upperAngularPath = Path.Combine(Directory.GetParent(solutionFolder)?.FullName ?? "", "angular"); @@ -340,5 +419,15 @@ public class PackagePreviewSwitcher : ITransientDependency public const string Short = "d"; public const string Long = "directory"; } + public static class Include + { + public const string Short = "i"; + public const string Long = "include"; + } + public static class Exclude + { + public const string Short = "ep"; + public const string Long = "exclude-packages"; + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs index 924d6041b1..f9aedb29b9 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs @@ -217,8 +217,8 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency } var currentVersion = versionAttribute.Value; - var isLeptonXPackage = packageId.Contains("LeptonX"); - var isStudioPackage = packageId.StartsWith("Volo.Abp.Studio."); + var isLeptonXPackage = IsLeptonXPackage(packageId); + var isStudioPackage = IsStudioPackage(packageId); if(isLeptonXPackage) { //'SemanticVersion.TryParse' can not parse the version if the version contains floating version resolution, such as '*-*' @@ -366,10 +366,107 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency return await Task.FromResult(content); } - private async Task GetLatestVersionFromMyGet(string packageId) + private static bool IsLeptonXPackage(string packageId) => packageId.Contains("LeptonX"); + + private static bool IsStudioPackage(string packageId) => packageId.StartsWith("Volo.Abp.Studio."); + + internal async Task GetLatestVersionFromMyGet(string packageId) { var myGetPack = await _myGetPackageListFinder.GetPackagesAsync(); return myGetPack.Packages.FirstOrDefault(p => p.Id == packageId)?.Versions.LastOrDefault(); } + + /// + /// Updates <PackageVersion Include="Volo.*"> entries in a Central Package Management + /// props file (e.g. Directory.Packages.props) to . + /// Regular PackageReference-based updates (UpdateSolutionAsync/UpdateProjectAsync) already + /// skip any PackageReference with no Version attribute (i.e. CPM-managed packages) - this + /// method is the explicit, opt-in counterpart for callers that also want those central + /// versions kept in sync. Not invoked unless a caller (e.g. the switch-to-nightly --include + /// option) explicitly requests it. + /// + public async Task UpdateCentralPackageVersionsAsync( + string filePath, + string latestVersionFromMyGet, + IEnumerable excludedPackageIds = null) + { + if (!File.Exists(filePath)) + { + Logger.LogWarning("--include file not found, skipped: {FilePath}", filePath); + return; + } + + if (latestVersionFromMyGet == null) + { + return; + } + + var excluded = new HashSet(excludedPackageIds ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); + + try + { + using (var fs = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + using (var sr = new StreamReader(fs, DefaultEncoding, true)) + { + var fileContent = await sr.ReadToEndAsync(); + + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(fileContent); + + var packageNodeList = doc.SelectNodes("//PackageVersion[starts-with(@Include, 'Volo.')]"); + if (packageNodeList != null) + { + foreach (XmlNode package in packageNodeList) + { + var packageId = package.Attributes?["Include"]?.Value; + if (packageId == null || excluded.Contains(packageId)) + { + continue; + } + + // LeptonX and Studio packages follow their own, independent version + // stream (see IsLeptonXPackage/IsStudioPackage, also used by + // UpdateVoloPackagesAsync above) - never stamp them with the + // Volo.Abp.Core anchor version, regardless of --exclude-packages. + if (IsLeptonXPackage(packageId) || IsStudioPackage(packageId)) + { + continue; + } + + var versionAttribute = package.Attributes["Version"]; + if (versionAttribute == null) + { + continue; + } + + if (versionAttribute.Value != latestVersionFromMyGet) + { + Logger.LogInformation("Updating central package \"{PackageId}\" from v{CurrentVersion} to v{LatestVersion}", packageId, versionAttribute.Value, latestVersionFromMyGet); + versionAttribute.Value = latestVersionFromMyGet; + } + } + } + + fs.Seek(0, SeekOrigin.Begin); + fs.SetLength(0); + + using (var sw = new StreamWriter(fs, DefaultEncoding)) + { + await sw.WriteAsync(doc.OuterXml); + await sw.FlushAsync(); + } + } + } + } + catch (Exception ex) + { + // The file is truncated before the updated XML is written back, so a failure here + // (disk full, process killed, file locked mid-write) can leave it empty/partially + // written on disk. Logged as an error (not a warning) so this isn't missed - the + // rest of the switch-to-nightly run still continues for other solutions/files. + Logger.LogError(ex, "Failed to update central package versions in \"{FilePath}\". The file may now be empty or partially written - please check it manually.", filePath); + } + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/ko.json b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/ko.json new file mode 100644 index 0000000000..23004a404f --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Localization/Resources/AbpDdd/ko.json @@ -0,0 +1,6 @@ +{ + "culture": "ko", + "texts": { + "MaxResultCountExceededExceptionMessage": "{0}은(는) {1}보다 클 수 없습니다! 더 많은 결과를 허용하려면 서버 측에서 {2}.{3}을(를) 늘리십시오." + } +} diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs index 3791c4202f..fa8aafc76a 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; using Volo.Abp.ObjectMapping; +using Volo.Abp.Threading; namespace Volo.Abp.Application.Services; @@ -44,6 +46,20 @@ public abstract class AbstractKeyReadOnlyAppService + /// Used by the to project the query to the . + /// The and the are not used while the query is projected. + /// + protected virtual IQueryProjector? GetOutputDtoQueryProjector + => LazyServiceProvider.LazyGetService>(); + + /// + /// Used by the to project the query to the . + /// The is not used while the query is projected. + /// + protected virtual IQueryProjector? GetListOutputDtoQueryProjector + => LazyServiceProvider.LazyGetService>(); + protected AbstractKeyReadOnlyAppService(IReadOnlyRepository repository) { ReadOnlyRepository = repository; @@ -53,6 +69,19 @@ public abstract class AbstractKeyReadOnlyAppService(id); + } + + return dtos[0]; + } + var entity = await GetEntityByIdAsync(id); return await MapToGetOutputDtoAsync(entity); @@ -65,7 +94,6 @@ public abstract class AbstractKeyReadOnlyAppService(); var entityDtos = new List(); if (totalCount > 0) @@ -73,8 +101,16 @@ public abstract class AbstractKeyReadOnlyAppService( @@ -85,6 +121,57 @@ public abstract class AbstractKeyReadOnlyAppService GetEntityByIdAsync(TKey id); + private CancellationToken GetCancellationToken() + { + return LazyServiceProvider + .LazyGetService(NullCancellationTokenProvider.Instance) + .FallbackToProvider(); + } + + /// + /// Should create a query that selects the entity with the given . + /// It returns null by default, then the entity is not projected. + /// + /// The id of the entity. + protected virtual Task?> CreateEntityQueryOrNullAsync(TKey id) + { + return Task.FromResult?>(null); + } + + /// + /// Projects the query of the entity with the given to the . + /// It uses the and the by default, + /// and the is used when it returns null. + /// Override it to await other queries, like the query of another aggregate root to join. + /// + /// The id of the entity. + protected virtual async Task?> CreateGetOutputDtoQueryOrNullAsync(TKey id) + { + var queryProjector = GetOutputDtoQueryProjector; + if (queryProjector == null) + { + return null; + } + + var query = await CreateEntityQueryOrNullAsync(id); + + return query == null ? null : queryProjector.ProjectTo(query); + } + + /// + /// Projects the given entity query to the . + /// It uses the by default, + /// and the is used when it returns null. + /// Override it to await other queries, like the query of another aggregate root to join. + /// The projection must return one row per entity: the total count is already calculated and the paging is + /// already applied, so adding or removing rows makes the page inconsistent with the total count. + /// + /// The sorted and paged entity query. + protected virtual Task?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable query) + { + return Task.FromResult(GetListOutputDtoQueryProjector?.ProjectTo(query)); + } + protected virtual async Task CheckGetPolicyAsync() { await CheckPolicyAsync(GetPolicyName); diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs index 672d85a2be..906b138fba 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs @@ -84,6 +84,13 @@ public abstract class CrudAppService?> CreateEntityQueryOrNullAsync(TKey id) + { + var query = await Repository.GetQueryableAsync(); + + return query.Where(e => e.Id!.Equals(id)); + } + protected override void MapToEntity(TUpdateInput updateInput, TEntity entity) { if (updateInput is IEntityDto entityDto) diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs index 867ca35ad4..c36d17ffc8 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs @@ -47,6 +47,13 @@ public abstract class ReadOnlyAppService?> CreateEntityQueryOrNullAsync(TKey id) + { + var query = await Repository.GetQueryableAsync(); + + return query.Where(e => e.Id!.Equals(id)); + } + protected override IQueryable ApplyDefaultSorting(IQueryable query) { if (typeof(TEntity).IsAssignableTo()) diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ko.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ko.json new file mode 100644 index 0000000000..1eef424242 --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ko.json @@ -0,0 +1,25 @@ +{ + "culture": "ko", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "기본 발신자 주소", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "기본 발신자 표시 이름", + "DisplayName:Abp.Mailing.Smtp.Host": "호스트", + "DisplayName:Abp.Mailing.Smtp.Port": "포트", + "DisplayName:Abp.Mailing.Smtp.UserName": "사용자 이름", + "DisplayName:Abp.Mailing.Smtp.Password": "비밀번호", + "DisplayName:Abp.Mailing.Smtp.Domain": "도메인", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "SSL 사용", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "기본 자격 증명 사용", + "Description:Abp.Mailing.DefaultFromAddress": "기본 발신자 주소입니다.", + "Description:Abp.Mailing.DefaultFromDisplayName": "기본 발신자 표시 이름입니다.", + "Description:Abp.Mailing.Smtp.Host": "SMTP 통신에 사용되는 호스트 이름 또는 IP 주소입니다.", + "Description:Abp.Mailing.Smtp.Port": "SMTP 통신에 사용되는 포트입니다.", + "Description:Abp.Mailing.Smtp.UserName": "자격 증명에 연결된 사용자 이름입니다.", + "Description:Abp.Mailing.Smtp.Password": "자격 증명에 연결된 사용자 이름의 비밀번호입니다.", + "Description:Abp.Mailing.Smtp.Domain": "자격 증명을 인증하는 도메인 또는 컴퓨터 이름입니다.", + "Description:Abp.Mailing.Smtp.EnableSsl": "SmtpClient가 SSL(Secure Sockets Layer)을 사용하여 연결을 암호화할지 여부입니다.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "요청과 함께 DefaultCredentials를 전송할지 여부입니다.", + "TextTemplate:StandardEmailTemplates.Layout": "기본 이메일 레이아웃 템플릿", + "TextTemplate:StandardEmailTemplates.Message": "이메일용 단순 메시지 템플릿" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ko.json b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ko.json new file mode 100644 index 0000000000..5af604d208 --- /dev/null +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ko.json @@ -0,0 +1,31 @@ +{ + "culture": "ko", + "texts": { + "InternalServerErrorMessage": "요청을 처리하는 동안 내부 오류가 발생했습니다!", + "ValidationErrorMessage": "요청이 유효하지 않습니다!", + "ValidationNarrativeErrorMessageTitle": "유효성 검사 중 다음 오류가 발견되었습니다.", + "DefaultErrorMessage": "오류가 발생했습니다!", + "DefaultErrorMessageDetail": "서버에서 오류 세부 정보를 보내지 않았습니다.", + "DefaultErrorMessage401": "인증되지 않았습니다!", + "DefaultErrorMessage401Detail": "이 작업을 수행하려면 로그인해야 합니다.", + "DefaultErrorMessage403": "권한이 없습니다!", + "DefaultErrorMessage403Detail": "이 작업을 수행할 권한이 없습니다!", + "DefaultErrorMessage404": "리소스를 찾을 수 없습니다!", + "DefaultErrorMessage404Detail": "서버에서 요청한 리소스를 찾을 수 없습니다!", + "EntityNotFoundErrorMessage": "{0} 엔터티 중 ID가 {1}인 항목이 없습니다!", + "EntityNotFoundErrorMessageWithoutId": "{0} 엔터티가 없습니다!", + "AbpDbConcurrencyErrorMessage": "제출한 데이터가 이미 다른 사용자에 의해 변경되었습니다. 변경 사항을 취소하고 다시 시도해 주세요.", + "Error": "오류", + "UnhandledException": "처리되지 않은 예외가 발생했습니다!", + "Authorizing": "권한을 확인하는 중…", + "401Message": "인증되지 않음", + "403Message": "접근 금지", + "404Message": "페이지를 찾을 수 없음", + "500Message": "내부 서버 오류", + "403MessageDetail": "이 작업을 수행할 권한이 없습니다!", + "404MessageDetail": "죄송합니다. 이 주소에 해당하는 페이지가 없습니다.", + "Unauthorized": "인증되지 않음", + "invalid_token": "유효하지 않은 토큰", + "SessionExpired": "세션이 만료되었습니다. 애플리케이션을 계속 사용하려면 다시 로그인해 주세요." + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/Localization/ko.json b/framework/src/Volo.Abp.Features/Volo/Abp/Features/Localization/ko.json new file mode 100644 index 0000000000..5dd9859e62 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/Localization/ko.json @@ -0,0 +1,8 @@ +{ + "culture": "ko", + "texts": { + "Volo.Feature:010001": "기능이 활성화되지 않았습니다: {FeatureName}", + "Volo.Feature:010002": "필수 기능이 활성화되지 않았습니다. 다음 기능을 모두 활성화해야 합니다: {FeatureNames}", + "Volo.Feature:010003": "필수 기능이 활성화되지 않았습니다. 다음 기능 중 하나 이상을 활성화해야 합니다: {FeatureNames}" + } +} diff --git a/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/Localization/ko.json b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/Localization/ko.json new file mode 100644 index 0000000000..5f4094051d --- /dev/null +++ b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/Localization/ko.json @@ -0,0 +1,6 @@ +{ + "culture": "ko", + "texts": { + "Volo.GlobalFeature:010001": "'{ServiceName}' 서비스에서 '{GlobalFeatureName}' 기능을 활성화해야 합니다." + } +} diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.Http.FluentValidation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1c556d5b2e --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Volo.Abp.Http.FluentValidation.Tests")] diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg.analyze.json b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg.analyze.json new file mode 100644 index 0000000000..cdcde838c3 --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.abppkg.analyze.json @@ -0,0 +1,68 @@ +{ + "name": "Volo.Abp.Http.FluentValidation", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.Http.FluentValidation", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.Http", + "namespace": "Volo.Abp.Http", + "name": "AbpHttpModule" + }, + { + "declaringAssemblyName": "Volo.Abp.FluentValidation", + "namespace": "Volo.Abp.FluentValidation", + "name": "AbpFluentValidationModule" + } + ], + "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": "AbpHttpFluentValidationModule", + "summary": null + } + ] +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj new file mode 100644 index 0000000000..5c44a55420 --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Volo.Abp.Http.FluentValidation.csproj @@ -0,0 +1,24 @@ + + + + + + + net8.0;net9.0;net10.0 + enable + Nullable + Volo.Abp.Http.FluentValidation + Volo.Abp.Http.FluentValidation + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationModule.cs b/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationModule.cs new file mode 100644 index 0000000000..b81f040620 --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.FluentValidation; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Http.FluentValidation; + +[DependsOn( + typeof(AbpHttpModule), + typeof(AbpFluentValidationModule) + )] +public class AbpHttpFluentValidationModule : AbpModule +{ +} diff --git a/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/FluentValidationPropertyApiDescriptionModelContributor.cs b/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/FluentValidationPropertyApiDescriptionModelContributor.cs new file mode 100644 index 0000000000..64d07bd021 --- /dev/null +++ b/framework/src/Volo.Abp.Http.FluentValidation/Volo/Abp/Http/FluentValidation/FluentValidationPropertyApiDescriptionModelContributor.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using FluentValidation; +using FluentValidation.Internal; +using FluentValidation.Validators; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Modeling; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Http.FluentValidation; + +[ExposeServices(typeof(IPropertyApiDescriptionModelContributor))] +public class FluentValidationPropertyApiDescriptionModelContributor : IPropertyApiDescriptionModelContributor, ITransientDependency +{ + private static readonly FrozenSet NumericTypes = new HashSet + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(IntPtr), + typeof(UIntPtr) + }.ToFrozenSet(); + + protected IServiceProvider ServiceProvider { get; } + + protected ConcurrentDictionary?> RuleCache { get; } + + public FluentValidationPropertyApiDescriptionModelContributor(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + RuleCache = new ConcurrentDictionary?>(); + } + + public virtual Task ContributeAsync(PropertyApiDescriptionModelContributionContext context) + { + Check.NotNull(context, nameof(context)); + + var rules = RuleCache.GetOrAdd(context.DeclaringType, GetUnconditionalRules); + if (rules == null) + { + return Task.CompletedTask; + } + + foreach (var validator in rules[context.Model.Name]) + { + ApplyValidator(context.Model, context.PropertyInfo, validator); + } + + return Task.CompletedTask; + } + + protected virtual ILookup? GetUnconditionalRules(Type declaringType) + { + if (declaringType.ContainsGenericParameters) + { + return null; + } + + var validator = ServiceProvider.GetService(typeof(IValidator<>).MakeGenericType(declaringType)) as IValidator; + if (validator == null) + { + return null; + } + + return validator + .CreateDescriptor() + .Rules + .Where(rule => IsUnconditional(rule) && DescribesPropertyOf(rule, declaringType)) + .SelectMany(rule => rule + .Components + .Where(component => !component.HasCondition && !component.HasAsyncCondition) + .Select(component => new KeyValuePair(rule.Member.Name, component.Validator))) + .ToLookup(x => x.Key, x => x.Value, StringComparer.Ordinal); + } + + protected virtual bool DescribesPropertyOf(IValidationRule rule, Type declaringType) + { + // The member is matched instead of the rule's property name, which OverridePropertyName + // and a custom PropertyNameResolver can change. A rule on a nested object declares its + // member on that object, so it must not be attributed to a property of this type. + return rule.Member != null && rule.Member.DeclaringType!.IsAssignableFrom(declaringType); + } + + protected virtual bool IsUnconditional(IValidationRule rule) + { + + // RuleForEach constrains the items, not the collection property itself. + if (IsCollectionRule(rule)) + { + return false; + } + + // A When(...)/Unless(...) block sets the condition on the rule, the chained form sets it on the components. + if (rule.HasCondition || rule.HasAsyncCondition) + { + return false; + } + + // Mirrors DefaultValidatorSelector: ABP never runs a rule that only belongs to a named rule set. + return rule.RuleSets.IsNullOrEmpty() || + rule.RuleSets!.Contains(RulesetValidatorSelector.DefaultRuleSetName, StringComparer.OrdinalIgnoreCase); + } + + protected virtual bool IsCollectionRule(IValidationRule rule) + { + return rule + .GetType() + .GetInterfaces() + .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollectionRule<,>)); + } + + protected virtual void ApplyValidator(PropertyApiDescriptionModel model, PropertyInfo propertyInfo, IPropertyValidator validator) + { + switch (validator) + { + case INotNullValidator: + case INotEmptyValidator: + model.IsRequired = true; + break; + case ILengthValidator lengthValidator: + ApplyLength(model, lengthValidator); + break; + case IRegularExpressionValidator regularExpressionValidator: + ApplyRegularExpression(model, regularExpressionValidator); + break; + case IBetweenValidator betweenValidator: + ApplyBetween(model, propertyInfo, betweenValidator); + break; + case IComparisonValidator comparisonValidator: + ApplyComparison(model, propertyInfo, comparisonValidator); + break; + } + } + + protected virtual void ApplyLength(PropertyApiDescriptionModel model, ILengthValidator validator) + { + // Every length validator has a Func form that leaves the bound at zero on the + // descriptor, so a zero is not distinguishable from a literal one and is left out. + if (validator.Min > 0) + { + model.MinLength = model.MinLength.HasValue + ? Math.Max(model.MinLength.Value, validator.Min) + : validator.Min; + } + + if (validator.Max > 0) + { + model.MaxLength = model.MaxLength.HasValue + ? Math.Min(model.MaxLength.Value, validator.Max) + : validator.Max; + } + } + + protected virtual void ApplyRegularExpression(PropertyApiDescriptionModel model, IRegularExpressionValidator validator) + { + // A single Regex field cannot express "must match all of them", so the first pattern wins. + if (!model.Regex.IsNullOrWhiteSpace() || validator.Expression.IsNullOrWhiteSpace()) + { + return; + } + + model.Regex = validator.Expression; + } + + protected virtual void ApplyComparison(PropertyApiDescriptionModel model, PropertyInfo propertyInfo, IComparisonValidator validator) + { + switch (validator.Comparison) + { + case Comparison.GreaterThan: + ApplyMinimum(model, propertyInfo, validator.ValueToCompare, isExclusive: true); + break; + case Comparison.GreaterThanOrEqual: + ApplyMinimum(model, propertyInfo, validator.ValueToCompare, isExclusive: false); + break; + case Comparison.LessThan: + ApplyMaximum(model, propertyInfo, validator.ValueToCompare, isExclusive: true); + break; + case Comparison.LessThanOrEqual: + ApplyMaximum(model, propertyInfo, validator.ValueToCompare, isExclusive: false); + break; + } + } + + protected virtual void ApplyBetween(PropertyApiDescriptionModel model, PropertyInfo propertyInfo, IBetweenValidator validator) + { + var from = GetNumericBound(propertyInfo, validator.From); + var to = GetNumericBound(propertyInfo, validator.To); + + // A between rule can carry its own comparer, which the descriptor does not expose. An + // interval that reads as empty in the natural order is what one looks like from here, + // and publishing its bounds would say the opposite of what the rule accepts. + if (from == null || to == null || !TryCompareBounds(from, to, out var comparison) || comparison > 0) + { + return; + } + + var isExclusive = validator is not IInclusiveBetweenValidator; + + ApplyMinimum(model, propertyInfo, validator.From, isExclusive); + ApplyMaximum(model, propertyInfo, validator.To, isExclusive); + } + + protected virtual void ApplyMinimum(PropertyApiDescriptionModel model, PropertyInfo propertyInfo, object? value, bool isExclusive) + { + var bound = GetNumericBound(propertyInfo, value); + if (bound == null) + { + return; + } + + if (model.Minimum != null) + { + if (!TryCompareBounds(model.Minimum, bound, out var comparison)) + { + return; + } + + // The higher bound wins, and an exclusive one is the stricter when both sit on the + // same value. The winning bound is published the way it was written, so no value is + // lost on the way through a number type that can not hold it. + if (comparison > 0 || (comparison == 0 && model.MinimumIsExclusive == true)) + { + return; + } + } + + model.Minimum = bound; + model.MinimumIsExclusive = isExclusive; + } + + protected virtual void ApplyMaximum(PropertyApiDescriptionModel model, PropertyInfo propertyInfo, object? value, bool isExclusive) + { + var bound = GetNumericBound(propertyInfo, value); + if (bound == null) + { + return; + } + + if (model.Maximum != null) + { + if (!TryCompareBounds(model.Maximum, bound, out var comparison)) + { + return; + } + + if (comparison < 0 || (comparison == 0 && model.MaximumIsExclusive == true)) + { + return; + } + } + + model.Maximum = bound; + model.MaximumIsExclusive = isExclusive; + } + + protected virtual string? GetNumericBound(PropertyInfo propertyInfo, object? value) + { + // Minimum and Maximum are numeric bounds. A comparison on another type, the ordinal + // comparison of two strings for example, means something else and can not go there. + // A comparison against another property has no value to publish either. + if (value == null || !NumericTypes.Contains(TypeHelper.StripNullable(propertyInfo.PropertyType))) + { + return null; + } + + var bound = Convert.ToString(value, CultureInfo.InvariantCulture); + return bound.IsNullOrWhiteSpace() || !double.TryParse(bound, NumberStyles.Float, CultureInfo.InvariantCulture, out _) + ? null + : bound; + } + + protected virtual bool TryCompareBounds(string left, string right, out int comparison) + { + // Decimal is exact for every integral type and for decimal itself, which double is not + // above its 53 bits of mantissa. Double only comes in for the magnitudes decimal can + // not hold, where its precision is the best there is anyway. + if (TryParseExactly(left, out var leftValue) && TryParseExactly(right, out var rightValue)) + { + comparison = leftValue.CompareTo(rightValue); + return true; + } + + if (double.TryParse(left, NumberStyles.Float, CultureInfo.InvariantCulture, out var leftDouble) && + double.TryParse(right, NumberStyles.Float, CultureInfo.InvariantCulture, out var rightDouble)) + { + comparison = leftDouble.CompareTo(rightDouble); + return true; + } + + comparison = 0; + return false; + } + + protected virtual bool TryParseExactly(string value, out decimal number) + { + if (!decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + { + return false; + } + + // A magnitude below the decimal range collapses to a zero, which would compare wrong. + return number != decimal.Zero || !value.Any(c => c is > '0' and <= '9'); + } +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs new file mode 100644 index 0000000000..a802b56409 --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IPropertyApiDescriptionModelContributor.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Http.Modeling; + +public interface IPropertyApiDescriptionModelContributor +{ + Task ContributeAsync(PropertyApiDescriptionModelContributionContext context); +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs index d0bf430546..a8d3f3ae5f 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Linq; using System.Reflection; using Volo.Abp.Http.ProxyScripting.Configuration; @@ -10,6 +13,23 @@ namespace Volo.Abp.Http.Modeling; [Serializable] public class PropertyApiDescriptionModel { + private static readonly HashSet NumericTypes = new HashSet + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(IntPtr), + typeof(UIntPtr) + }; + public string Name { get; set; } = default!; public string? JsonName { get; set; } @@ -28,6 +48,10 @@ public class PropertyApiDescriptionModel public string? Maximum { get; set; } + public bool? MinimumIsExclusive { get; set; } + + public bool? MaximumIsExclusive { get; set; } + public string? Regex { get; set; } public bool IsNullable { get; set; } @@ -41,6 +65,7 @@ public class PropertyApiDescriptionModel public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo) { var customAttributes = propertyInfo.GetCustomAttributes(true); + var rangeAttribute = customAttributes.OfType().FirstOrDefault(); return new PropertyApiDescriptionModel { Name = propertyInfo.Name, @@ -49,11 +74,95 @@ public class PropertyApiDescriptionModel TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(propertyInfo.PropertyType), IsRequired = customAttributes.OfType().Any() || propertyInfo.GetCustomAttributesData().Any(attr => attr.AttributeType.Name == "RequiredMemberAttribute"), IsNullable = ReflectionHelper.IsNullable(propertyInfo), - Minimum = customAttributes.OfType().Select(x => x.Minimum).FirstOrDefault()?.ToString(), - Maximum = customAttributes.OfType().Select(x => x.Maximum).FirstOrDefault()?.ToString(), + Minimum = GetRangeBound(rangeAttribute, rangeAttribute?.Minimum), + Maximum = GetRangeBound(rangeAttribute, rangeAttribute?.Maximum), + MinimumIsExclusive = GetMinimumIsExclusive(rangeAttribute), + MaximumIsExclusive = GetMaximumIsExclusive(rangeAttribute), MinLength = customAttributes.OfType().FirstOrDefault()?.Length ?? customAttributes.OfType().FirstOrDefault()?.MinimumLength, MaxLength = customAttributes.OfType().FirstOrDefault()?.Length ?? customAttributes.OfType().FirstOrDefault()?.MaximumLength, Regex= customAttributes.OfType().Select(x => x.Pattern).FirstOrDefault() }; } + + private static string? GetRangeBound(RangeAttribute? rangeAttribute, object? bound) + { + if (rangeAttribute == null || bound == null) + { + return null; + } + + // The Range(Type, string, string) constructor keeps its limits as strings until the + // first validation. Converting one the way the attribute converts it reports the value + // the attribute validates against, which is not always the value that was written down. + // The attribute reads its limits in the culture of the request unless it opts into the + // invariant one, so only that opt-in makes the reported limit stable across requests. + if (bound is string text) + { + if (!NumericTypes.Contains(rangeAttribute.OperandType)) + { + return text; + } + + var converted = ConvertRangeLimitOrNull(text, rangeAttribute); + return converted != null + ? Convert.ToString(converted, CultureInfo.InvariantCulture) + : text; + } + + return Convert.ToString(bound, CultureInfo.InvariantCulture); + } + + private static object? ConvertRangeLimitOrNull(string text, RangeAttribute rangeAttribute) + { + try + { + return TypeDescriptor + .GetConverter(rangeAttribute.OperandType) + .ConvertFromString(null, GetRangeLimitCulture(rangeAttribute), text); + } + catch (Exception) + { + // A limit that does not convert is reported the way it was written. The attribute + // throws on it during the first validation, and failing the whole api definition + // over one declaration would hide every other type. + return null; + } + } + + private static CultureInfo GetRangeLimitCulture(RangeAttribute rangeAttribute) + { +#if NET8_0_OR_GREATER + return rangeAttribute.ParseLimitsInInvariantCulture ? CultureInfo.InvariantCulture : CultureInfo.CurrentCulture; +#else + return CultureInfo.CurrentCulture; +#endif + } + + private static bool? GetMinimumIsExclusive(RangeAttribute? rangeAttribute) + { + if (rangeAttribute == null) + { + return null; + } + +#if NET8_0_OR_GREATER + return rangeAttribute.MinimumIsExclusive; +#else + return false; +#endif + } + + private static bool? GetMaximumIsExclusive(RangeAttribute? rangeAttribute) + { + if (rangeAttribute == null) + { + return null; + } + +#if NET8_0_OR_GREATER + return rangeAttribute.MaximumIsExclusive; +#else + return false; +#endif + } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs new file mode 100644 index 0000000000..d4acf2c0b4 --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModelContributionContext.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using JetBrains.Annotations; + +namespace Volo.Abp.Http.Modeling; + +public class PropertyApiDescriptionModelContributionContext +{ + [NotNull] + public PropertyApiDescriptionModel Model { get; } + + [NotNull] + public PropertyInfo PropertyInfo { get; } + + [NotNull] + public Type DeclaringType { get; } + + public PropertyApiDescriptionModelContributionContext( + [NotNull] PropertyApiDescriptionModel model, + [NotNull] PropertyInfo propertyInfo, + [NotNull] Type declaringType) + { + Model = Check.NotNull(model, nameof(model)); + PropertyInfo = Check.NotNull(propertyInfo, nameof(propertyInfo)); + DeclaringType = Check.NotNull(declaringType, nameof(declaringType)); + } +} diff --git a/framework/src/Volo.Abp.Ldap.Abstractions/Volo/Abp/Ldap/Localization/ko.json b/framework/src/Volo.Abp.Ldap.Abstractions/Volo/Abp/Ldap/Localization/ko.json new file mode 100644 index 0000000000..947eef8900 --- /dev/null +++ b/framework/src/Volo.Abp.Ldap.Abstractions/Volo/Abp/Ldap/Localization/ko.json @@ -0,0 +1,19 @@ +{ + "culture": "ko", + "texts": { + "DisplayName:Abp.Ldap.Ldaps": "SSL을 통한 LDAP", + "Description:Abp.Ldap.Ldaps": "SSL을 통한 LDAP", + "DisplayName:Abp.Ldap.ServerHost": "서버 호스트", + "Description:Abp.Ldap.ServerHost": "서버 호스트", + "DisplayName:Abp.Ldap.ServerPort": "서버 포트", + "Description:Abp.Ldap.ServerPort": "서버 포트", + "DisplayName:Abp.Ldap.BaseDc": "기본 도메인 구성 요소", + "Description:Abp.Ldap.BaseDc": "기본 도메인 구성 요소", + "DisplayName:Abp.Ldap.Domain": "도메인", + "Description:Abp.Ldap.Domain": "도메인", + "DisplayName:Abp.Ldap.UserName": "사용자 이름", + "Description:Abp.Ldap.UserName": "사용자 이름", + "DisplayName:Abp.Ldap.Password": "비밀번호", + "Description:Abp.Ldap.Password": "비밀번호" + } +} diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/ko.json b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/ko.json new file mode 100644 index 0000000000..f373dfc4b5 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Resources/AbpLocalization/ko.json @@ -0,0 +1,7 @@ +{ + "culture": "ko", + "texts": { + "DisplayName:Abp.Localization.DefaultLanguage": "기본 언어", + "Description:Abp.Localization.DefaultLanguage": "애플리케이션의 기본 언어입니다." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css index 5fa2c86435..61601954a9 100644 --- a/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css +++ b/framework/src/Volo.Abp.MudBlazorUI/wwwroot/volo.abp.mudblazorui.css @@ -110,3 +110,24 @@ padding-inline-end: 16px; } + + +/* Password visibility toggle: MudBlazor renders the input end-adornment with a hard-coded + tabindex="-1", so a toggle placed there can never be reached by keyboard. The toggle is + rendered as a normal MudIconButton next to the field and pinned over the input row instead. + The offset is measured from the top of the control because the label sits above the input row + and validation text is rendered below it, so the control's own height is not a stable anchor. */ +.abp-password-field { + position: relative; +} + +.abp-password-field .abp-password-visibility-toggle { + position: absolute; + inset-inline-end: 0; + top: 38px; + transform: translateY(-50%); +} + +.abp-password-field .mud-input:not(.mud-select-input) .mud-input-slot.mud-input-root { + padding-inline-end: 40px !important; +} diff --git a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/Localization/ko.json b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/Localization/ko.json new file mode 100644 index 0000000000..aa003999a1 --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/Localization/ko.json @@ -0,0 +1,9 @@ +{ + "culture": "ko", + "texts": { + "TenantNotFoundMessage": "테넌트를 찾을 수 없습니다!", + "TenantNotFoundDetails": "다음 테넌트 ID 또는 이름에 해당하는 테넌트가 없습니다: {0}", + "TenantNotActiveMessage": "테넌트가 활성 상태가 아닙니다!", + "TenantNotActiveDetails": "다음 테넌트 ID 또는 이름에 해당하는 테넌트가 활성 상태가 아닙니다: {0}" + } +} diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs index e572d6e155..2f1b8830ac 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; using Volo.Abp.Reflection; @@ -18,6 +19,14 @@ public class AbpObjectMappingModule : AbpModule typeof(IObjectMapper<,>) ).ConvertAll(t => new ServiceIdentifier(t)) ); + + //Register types for IQueryProjector if implements + foreach (var serviceType in ReflectionHelper.GetImplementedGenericTypes( + onServiceExposingContext.ImplementationType, + typeof(IQueryProjector<,>))) + { + onServiceExposingContext.ExposedTypes.AddIfNotContains(new ServiceIdentifier(serviceType)); + } }); } diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs new file mode 100644 index 0000000000..89652e5f40 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/IQueryProjector.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.ObjectMapping; + +/// +/// Maps a query to another. +/// Implement this interface to project a query on the data store side, instead of loading the +/// source objects into the memory and mapping them one by one. +/// Implement it once for a source and destination pair. Use the ReplaceServices option of the +/// DependencyAttribute to replace an existing implementation. +/// +/// Type of the source objects +/// Type of the destination objects +public interface IQueryProjector : ITransientDependency +{ + /// + /// Projects the given query. The returned query must be built on top of it and must keep its order, + /// with a single destination object for each source object, using expressions the query provider can + /// translate. The caller may have already sorted, paged or counted the source query. + /// + /// The query to project + IQueryable ProjectTo(IQueryable source); +} diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ko.json b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ko.json new file mode 100644 index 0000000000..b0bfb72be9 --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ko.json @@ -0,0 +1,7 @@ +{ + "culture": "ko", + "texts": { + "DisplayName:Abp.Timing.Timezone": "시간대", + "Description:Abp.Timing.Timezone": "애플리케이션 시간대" + } +} diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/ko.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/ko.json new file mode 100644 index 0000000000..a3b9b1296e --- /dev/null +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/ko.json @@ -0,0 +1,6 @@ +{ + "culture": "ko", + "texts": { + "Menu:Administration": "관리" + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json index eb32a44fa2..8d5370f95e 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -62,6 +62,7 @@ "Theme": "سمة", "NotAssigned": "غيرمعتمد", "EntityActionsDisabledTooltip": "ليس لديك إذن لتنفيذ أي إجراء.", - "ResourcePermissions": "أذونات" + "ResourcePermissions": "أذونات", + "ShowPassword": "عرض كلمة المرور" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json index 27a174cb49..5d50dc3163 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json @@ -62,6 +62,7 @@ "Theme": "Téma", "NotAssigned": "Nepřiřazena", "EntityActionsDisabledTooltip": "Nemáte oprávnění provést žádnou akci.", - "ResourcePermissions": "Oprávnění" + "ResourcePermissions": "Oprávnění", + "ShowPassword": "Zobrazit heslo" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json index 869d049be1..e2e4c9a47c 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json @@ -62,6 +62,7 @@ "Theme": "Thema", "NotAssigned": "Nicht zugeordnet", "EntityActionsDisabledTooltip": "Sie haben keine Berechtigung, Aktionen auszuführen.", - "ResourcePermissions": "Berechtigungen" + "ResourcePermissions": "Berechtigungen", + "ShowPassword": "Passwort anzeigen" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json index 8c2dc2be1d..a930666e39 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json @@ -57,5 +57,6 @@ "Apply": "Ισχύουν", "EntityActionsDisabledTooltip": "Δεν έχετε δικαίωμα να εκτελέσετε καμία ενέργεια.", "ResourcePermissions": "Δικαιώματα", + "ShowPassword": "Εμφάνιση κωδικού πρόσβασης" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json index ce9d962d5e..f1a2bf02c5 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json @@ -57,6 +57,7 @@ "Today": "Today", "Apply": "Apply", "EntityActionsDisabledTooltip": "You do not have permission to perform any action.", - "ResourcePermissions": "Permissions" + "ResourcePermissions": "Permissions", + "ShowPassword": "Show password" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json index f328765a39..0bf0a49a17 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json @@ -62,6 +62,7 @@ "Theme": "Theme", "NotAssigned": "Not Assigned", "EntityActionsDisabledTooltip": "You do not have permission to perform any action.", - "ResourcePermissions": "Permissions" + "ResourcePermissions": "Permissions", + "ShowPassword": "Show password" } } 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 9d341cb88f..fd8a9006ea 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "No asignado", "EntityActionsDisabledTooltip": "No tienes permisos para realizar ninguna acción.", - "ResourcePermissions": "Permisos" + "ResourcePermissions": "Permisos", + "ShowPassword": "Mostrar contraseña" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json index 5862eab90e..07ca9e5f9b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json @@ -56,6 +56,7 @@ "Today": "امروز", "Apply": "درخواست دادن", "EntityActionsDisabledTooltip": "شما دسترسی به انجام هر گونه عملیات ندارید.", - "ResourcePermissions": "مجوزها" + "ResourcePermissions": "مجوزها", + "ShowPassword": "نمایش رمز عبور" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json index 51e356559b..bc7ed9c5a2 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json @@ -62,6 +62,7 @@ "Theme": "Teema", "NotAssigned": "Ei määritetty", "EntityActionsDisabledTooltip": "Sinulla ei ole oikeutta suorittaa mitään toimintoa.", - "ResourcePermissions": "Käyttöoikeudet" + "ResourcePermissions": "Käyttöoikeudet", + "ShowPassword": "Näytä salasana" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json index 79a40c57f9..0098158a0a 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json @@ -62,6 +62,7 @@ "Theme": "Thème", "NotAssigned": "Non attribué", "EntityActionsDisabledTooltip": "Vous n'avez pas les permissions pour effectuer une action.", - "ResourcePermissions": "Autorisations" + "ResourcePermissions": "Autorisations", + "ShowPassword": "Afficher le mot de passe" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json index 10c1e426c1..a0c0a7f233 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json @@ -62,6 +62,7 @@ "Theme": "विषय", "NotAssigned": "सौंपा नहीं गया है", "EntityActionsDisabledTooltip": "आपके पास कोई कार्रवाई नहीं है जो करने के लिए है।", - "ResourcePermissions": "अनुमतियाँ" + "ResourcePermissions": "अनुमतियाँ", + "ShowPassword": "पासवर्ड दिखाएं" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json index c36751828c..ea35dfa8ec 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "Nije dodijeljeno", "EntityActionsDisabledTooltip": "Nemate dozvolu za izvođenje bilo kakve akcije.", - "ResourcePermissions": "Dozvole" + "ResourcePermissions": "Dozvole", + "ShowPassword": "Pokaži lozinku" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json index e162a2c6c8..cdc71838f5 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json @@ -62,6 +62,7 @@ "Theme": "Téma", "NotAssigned": "Nem kijelölt", "EntityActionsDisabledTooltip": "Nincs jogosultsága bármely művelethez.", - "ResourcePermissions": "Engedélyek" + "ResourcePermissions": "Engedélyek", + "ShowPassword": "Jelszó megjelenítése" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json index 3d88d70d97..df5123de9e 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json @@ -62,6 +62,7 @@ "Theme": "Þema", "NotAssigned": "Ekki skráður", "EntityActionsDisabledTooltip": "Þú hefur ekki aðgang að þessum aðgerðum.", - "ResourcePermissions": "Aðgangsheimildir" + "ResourcePermissions": "Aðgangsheimildir", + "ShowPassword": "Sýna lykilorð" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json index 9bfffe181e..d866284043 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "Non assegnato", "EntityActionsDisabledTooltip": "Non hai i permessi per eseguire alcuna azione.", - "ResourcePermissions": "Autorizzazioni" + "ResourcePermissions": "Autorizzazioni", + "ShowPassword": "Mostra password" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ko.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ko.json new file mode 100644 index 0000000000..0be3c1219a --- /dev/null +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ko.json @@ -0,0 +1,68 @@ +{ + "culture": "ko", + "texts": { + "Languages": "언어", + "AreYouSure": "계속하시겠습니까?", + "Cancel": "취소", + "Clear": "지우기", + "Yes": "예", + "No": "아니요", + "Ok": "확인", + "Close": "닫기", + "Save": "저장", + "SavingWithThreeDot": "저장 중...", + "Actions": "작업", + "Delete": "삭제", + "CreatedSuccessfully": "생성되었습니다.", + "SavedSuccessfully": "저장되었습니다.", + "DeletedSuccessfully": "삭제되었습니다.", + "Edit": "편집", + "Refresh": "새로 고침", + "Language": "언어", + "LoadMore": "더 보기", + "ProcessingWithThreeDot": "처리 중...", + "LoadingWithThreeDot": "불러오는 중...", + "Welcome": "환영합니다.", + "Login": "로그인", + "Register": "회원가입", + "Logout": "로그아웃", + "Submit": "제출", + "Back": "뒤로", + "PagerSearch": "검색", + "PagerNext": "다음", + "PagerPrevious": "이전", + "PagerFirst": "처음", + "PagerLast": "마지막", + "PagerInfo": "총 _TOTAL_개 항목 중 _START_~_END_개를 표시합니다.", + "PagerInfo{0}{1}{2}": "총 {2}개 항목 중 {0}~{1}개를 표시합니다.", + "PagerInfoEmpty": "총 0개 항목 중 0~0개를 표시합니다.", + "PagerInfoFiltered": "(총 _MAX_개 항목에서 필터링됨)", + "NoDataAvailableInDatatable": "사용 가능한 데이터가 없습니다.", + "ErrorLoadingDatatable": "요청 중 오류가 발생했습니다. 자세한 내용은 메시지를 확인하십시오.", + "Total": "전체", + "Selected": "선택됨", + "PagerShowMenuEntries": "_MENU_개 항목 표시", + "DatatableActionDropdownDefaultText": "작업", + "ChangePassword": "비밀번호 변경", + "PersonalInfo": "내 프로필", + "AreYouSureYouWantToCancelEditingWarningMessage": "저장하지 않은 변경 사항이 있습니다.", + "GoHomePage": "홈페이지로 이동", + "GoBack": "뒤로 이동", + "Search": "검색", + "ItemWillBeDeletedMessageWithFormat": "{0}이(가) 삭제됩니다!", + "ItemWillBeDeletedMessage": "이 항목이 삭제됩니다!", + "ManageYourAccount": "계정 관리", + "OthersGroup": "기타", + "Today": "오늘", + "Apply": "적용", + "InternetConnectionInfo": "현재 인터넷에 연결되어 있지 않아 작업을 수행할 수 없습니다.", + "CopiedToTheClipboard": "클립보드에 복사되었습니다.", + "AddNew": "새로 추가", + "ProfilePicture": "프로필 사진", + "Theme": "테마", + "NotAssigned": "할당되지 않음", + "EntityActionsDisabledTooltip": "작업을 수행할 권한이 없습니다.", + "ResourcePermissions": "권한", + "ShowPassword": "비밀번호 표시" + } +} diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json index 51ab0bcd31..22366cf991 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json @@ -62,6 +62,7 @@ "Theme": "Thema", "NotAssigned": "Niet toegekend", "EntityActionsDisabledTooltip": "U hebt geen toegang tot deze acties.", - "ResourcePermissions": "Machtigingen" + "ResourcePermissions": "Machtigingen", + "ShowPassword": "Wachtwoord tonen" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json index 20b4daf7f0..be79d9849c 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json @@ -62,6 +62,7 @@ "Theme": "Temat", "NotAssigned": "Nie przypisano", "EntityActionsDisabledTooltip": "Nie masz uprawnień do wykonania żadnej akcji.", - "ResourcePermissions": "Uprawnienia" + "ResourcePermissions": "Uprawnienia", + "ShowPassword": "Pokaż hasło" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json index 9764d99fe6..f6d66e07c3 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "Não atribuído", "EntityActionsDisabledTooltip": "Você não tem permissão para executar qualquer ação.", - "ResourcePermissions": "Permissões" + "ResourcePermissions": "Permissões", + "ShowPassword": "Mostrar senha" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json index 6b7367994c..f70b0f4a6b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json @@ -62,6 +62,7 @@ "Theme": "Temă", "NotAssigned": "Nealocat", "EntityActionsDisabledTooltip": "Nu aveți permisiune să efectuați nicio acțiune.", - "ResourcePermissions": "Permisiuni" + "ResourcePermissions": "Permisiuni", + "ShowPassword": "Afișează parola" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json index d4f4acf5d6..baaf43bcb8 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json @@ -62,6 +62,7 @@ "Theme": "Тема", "NotAssigned": "Не назначен", "EntityActionsDisabledTooltip": "У вас нет прав на выполнение каких-либо действий.", - "ResourcePermissions": "Разрешения" + "ResourcePermissions": "Разрешения", + "ShowPassword": "Показать пароль" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json index 59d8aecef4..1e6c09fd51 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json @@ -62,6 +62,7 @@ "Theme": "Téma", "NotAssigned": "Nepridelené", "EntityActionsDisabledTooltip": "Nemáte oprávnenie vykonávať žiadnu akciu.", - "ResourcePermissions": "Oprávnenia" + "ResourcePermissions": "Oprávnenia", + "ShowPassword": "Zobraziť heslo" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json index 89f2691a11..3be6d1d83c 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "Ni dodeljena", "EntityActionsDisabledTooltip": "Nimate pravic za izvajanje kakršne koli dejanje.", - "ResourcePermissions": "Dovoljenja" + "ResourcePermissions": "Dovoljenja", + "ShowPassword": "Pokaži geslo" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sv.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sv.json index fe4b692cdf..b44d00d22f 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sv.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sv.json @@ -61,6 +61,7 @@ "Theme": "Tema", "NotAssigned": "Ej tilldelad", "EntityActionsDisabledTooltip": "Du har inte tillgång till dessa åtgärder.", - "ResourcePermissions": "Behörigheter" + "ResourcePermissions": "Behörigheter", + "ShowPassword": "Visa lösenord" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json index 73a4bb5cb4..a998d72c41 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json @@ -62,6 +62,7 @@ "Theme": "Tema", "NotAssigned": "Atanmadı", "EntityActionsDisabledTooltip": "Bu işlemi gerçekleştirmek için yeterli yetkiniz yok.", - "ResourcePermissions": "İzinler" + "ResourcePermissions": "İzinler", + "ShowPassword": "Şifreyi göster" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json index 015ed883d3..40ba15446d 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json @@ -62,6 +62,7 @@ "Theme": "chủ đề", "NotAssigned": "Không được chỉ định", "EntityActionsDisabledTooltip": "Bạn không có quyền thực hiện bất kỳ hành động nào.", - "ResourcePermissions": "Quyền" + "ResourcePermissions": "Quyền", + "ShowPassword": "Hiện mật khẩu" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json index b0759d7da9..bf0481cfc8 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json @@ -62,6 +62,7 @@ "Theme": "主题", "NotAssigned": "未分配", "EntityActionsDisabledTooltip": "您没有权限执行任何操作。", - "ResourcePermissions": "权限" + "ResourcePermissions": "权限", + "ShowPassword": "显示密码" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json index e35bae3610..c534300915 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json @@ -62,6 +62,7 @@ "Theme": "主題", "NotAssigned": "未分配", "EntityActionsDisabledTooltip": "您沒有權限執行任何操作。", - "ResourcePermissions": "權限" + "ResourcePermissions": "權限", + "ShowPassword": "顯示密碼" } } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ko.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ko.json new file mode 100644 index 0000000000..9f22a0965e --- /dev/null +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ko.json @@ -0,0 +1,39 @@ +{ + "culture": "ko", + "texts": { + "'{0}' and '{1}' do not match.": "'{0}'와(과) '{1}'이(가) 일치하지 않습니다.", + "The {0} field is not a valid credit card number.": "{0} 필드는 유효한 신용카드 번호가 아닙니다.", + "{0} is not valid.": "{0}은(는) 유효하지 않습니다.", + "The {0} field is not a valid e-mail address.": "{0} 필드는 유효한 이메일 주소가 아닙니다.", + "The {0} field only accepts files with the following extensions: {1}": "{0} 필드는 다음 확장자를 가진 파일만 허용합니다: {1}", + "The field {0} must be a string or array type with a maximum length of '{1}'.": "{0} 필드는 최대 길이가 '{1}'인 문자열 또는 배열 형식이어야 합니다.", + "The field {0} must be a string or array type with a minimum length of '{1}'.": "{0} 필드는 최소 길이가 '{1}'인 문자열 또는 배열 형식이어야 합니다.", + "The {0} field is not a valid phone number.": "{0} 필드는 유효한 전화번호가 아닙니다.", + "The field {0} must be between {1} and {2}.": "{0} 필드는 {1}에서 {2} 사이여야 합니다.", + "The field {0} must match the regular expression '{1}'.": "{0} 필드는 정규식 '{1}'과(와) 일치해야 합니다.", + "The {0} field is required.": "{0} 필드는 필수입니다.", + "The field {0} must be a string with a maximum length of {1}.": "{0} 필드는 최대 길이가 {1}인 문자열이어야 합니다.", + "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "{0} 필드는 최소 길이가 {2}, 최대 길이가 {1}인 문자열이어야 합니다.", + "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "{0} 필드는 유효한 완전한 형식의 http, https 또는 ftp URL이 아닙니다.", + "The field {0} is invalid.": "{0} 필드가 유효하지 않습니다.", + "The value '{0}' is invalid.": "값 '{0}'은(는) 유효하지 않습니다.", + "The field {0} must be a number.": "{0} 필드는 숫자여야 합니다.", + "The field must be a number.": "이 필드는 숫자여야 합니다.", + "ThisFieldIsNotAValidCreditCardNumber.": "이 필드는 유효한 신용카드 번호가 아닙니다.", + "ThisFieldIsNotValid.": "이 필드는 유효하지 않습니다.", + "ThisFieldIsNotAValidEmailAddress.": "이 필드는 유효한 이메일 주소가 아닙니다.", + "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "이 필드는 다음 확장자를 가진 파일만 허용합니다: {0}", + "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}": "이 필드는 최대 '{0}'자여야 합니다.", + "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "이 필드는 최소 '{0}'자여야 합니다.", + "ThisFieldIsNotAValidPhoneNumber.": "이 필드는 유효한 전화번호가 아닙니다.", + "ThisFieldMustBeBetween{0}And{1}": "이 필드는 {0}에서 {1} 사이여야 합니다.", + "ThisFieldMustBeGreaterThanOrEqual{0}": "이 필드는 {0} 이상이어야 합니다.", + "ThisFieldMustBeLessOrEqual{0}": "이 필드는 {0} 이하여야 합니다.", + "ThisFieldMustMatchTheRegularExpression{0}": "이 필드는 정규식 '{0}'과(와) 일치해야 합니다.", + "ThisFieldIsRequired.": "이 필드는 필수입니다.", + "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "이 필드는 최대 '{0}'자여야 합니다.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "이 필드는 최소 {1}자, 최대 {0}자여야 합니다.", + "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "이 필드는 유효한 완전한 형식의 http, https 또는 ftp URL이 아닙니다.", + "ThisFieldIsInvalid.": "이 필드는 유효하지 않습니다." + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj index 496dba12ed..7eb833e09a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo.Abp.AspNetCore.Mvc.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs index 4339104b30..a8707a8c38 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcTestModule.cs @@ -24,7 +24,7 @@ using Volo.Abp.TestApp; using Volo.Abp.TestApp.Application; using Volo.Abp.Threading; using Volo.Abp.Validation.Localization; -using Volo.Abp.FluentValidation; +using Volo.Abp.Http.FluentValidation; using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.AspNetCore.Mvc; @@ -34,7 +34,7 @@ namespace Volo.Abp.AspNetCore.Mvc; typeof(AbpMemoryDbTestModule), typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpFluentValidationModule) + typeof(AbpHttpFluentValidationModule) )] public class AbpAspNetCoreMvcTestModule : AbpModule { diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs index 5404555ffb..3ed8f1c7ad 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Tests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Http.Modeling; +using Volo.Abp.TestApp.Application; using Xunit; namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; @@ -75,6 +76,20 @@ public class AbpApiDefinitionController_Tests : AspNetCoreMvcTestBase action.AuthorizeDatas.ShouldContain(a => a.Policy == "TestPolicy2" && a.Roles == "Manager"); } + [Fact] + public async Task Should_Include_FluentValidation_Rules_In_Types() + { + var model = await GetResponseAsObjectAsync("/api/abp/api-definition?includeTypes=true"); + + var property = model.Types[typeof(FluentValidationTestInput).FullName!] + .Properties! + .Single(p => p.Name == nameof(FluentValidationTestInput.Name)); + + property.IsRequired.ShouldBeTrue(); + property.MinLength.ShouldBe(3); + property.MaxLength.ShouldBe(10); + } + private static ControllerApiDescriptionModel GetPeopleController(ApplicationApiDescriptionModel model) { return model.Modules.Values diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/Book.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/Book.cs new file mode 100644 index 0000000000..6211160df4 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/Book.cs @@ -0,0 +1,23 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class Book : Entity +{ + public string Name { get; set; } = default!; + + public int Price { get; set; } + + public Book() + { + + } + + public Book(Guid id, string name, int price) + : base(id) + { + Name = name; + Price = price; + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyAppService.cs new file mode 100644 index 0000000000..1110ff841e --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyAppService.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookAbstractKeyAppService : AbstractKeyReadOnlyAppService +{ + public BookAbstractKeyAppService(IReadOnlyRepository repository) + : base(repository) + { + + } + + protected override async Task GetEntityByIdAsync(Guid id) + { + var query = await ReadOnlyRepository.GetQueryableAsync(); + + return await AsyncExecuter.FirstAsync(query, book => book.Id == id); + } + + protected override IQueryable ApplyDefaultSorting(IQueryable query) + { + return query.OrderBy(book => book.Id); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyProjectingAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyProjectingAppService.cs new file mode 100644 index 0000000000..c6bcf8dbce --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAbstractKeyProjectingAppService.cs @@ -0,0 +1,35 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookAbstractKeyProjectingAppService : AbstractKeyReadOnlyAppService +{ + public BookAbstractKeyProjectingAppService(IReadOnlyRepository repository) + : base(repository) + { + + } + + protected override async Task GetEntityByIdAsync(Guid id) + { + var query = await ReadOnlyRepository.GetQueryableAsync(); + + return await AsyncExecuter.FirstAsync(query, book => book.Id == id); + } + + protected override async Task?> CreateEntityQueryOrNullAsync(Guid id) + { + var query = await ReadOnlyRepository.GetQueryableAsync(); + + return query.Where(book => book.Id == id); + } + + protected override IQueryable ApplyDefaultSorting(IQueryable query) + { + return query.OrderBy(book => book.Id); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAppService.cs new file mode 100644 index 0000000000..c3ae530e7d --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookAppService : CrudAppService +{ + public BookAppService(IRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAsyncProjectionAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAsyncProjectionAppService.cs new file mode 100644 index 0000000000..21f7516116 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookAsyncProjectionAppService.cs @@ -0,0 +1,46 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookAsyncProjectionAppService : CrudAppService +{ + public const string Marker = "-async"; + + private readonly IBookNameSuffixProvider _suffixProvider; + + public BookAsyncProjectionAppService( + IRepository repository, + IBookNameSuffixProvider suffixProvider) + : base(repository) + { + _suffixProvider = suffixProvider; + } + + protected override async Task?> CreateGetOutputDtoQueryOrNullAsync(Guid id) + { + var query = await Repository.GetQueryableAsync(); + + return await ProjectAsync(query.Where(book => book.Id == id)); + } + + protected override async Task?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable query) + { + return await ProjectAsync(query); + } + + private async Task> ProjectAsync(IQueryable query) + { + var suffix = await _suffixProvider.GetAsync(); + + return query.Select(book => new BookDto + { + Id = book.Id, + Name = book.Name + suffix + }); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs new file mode 100644 index 0000000000..f5df26baaa --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookCustomizedAppService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookCustomizedAppService : CrudAppService +{ + public const string Marker = "-customized"; + + public BookCustomizedAppService(IRepository repository) + : base(repository) + { + + } + + protected override async Task GetEntityByIdAsync(Guid id) + { + var book = await base.GetEntityByIdAsync(id); + book.Name += Marker; + return book; + } + + protected override Task MapToGetOutputDtoAsync(Book entity) + { + return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); + } + + protected override Task> MapToGetListOutputDtosAsync(List entities) + { + return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailAppService.cs new file mode 100644 index 0000000000..ec2139a118 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailAppService.cs @@ -0,0 +1,15 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookDetailAppService : + ReadOnlyAppService +{ + public BookDetailAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailDto.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailDto.cs new file mode 100644 index 0000000000..f278d6d721 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailDto.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookDetailDto : EntityDto +{ + public string Name { get; set; } = default!; +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailProjector.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailProjector.cs new file mode 100644 index 0000000000..d7e3e19966 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDetailProjector.cs @@ -0,0 +1,18 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookDetailProjector : IQueryProjector +{ + public const string Marker = "-detail"; + + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(book => new BookDetailDto + { + Id = book.Id, + Name = book.Name + Marker + }); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDto.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDto.cs new file mode 100644 index 0000000000..08a2117ec2 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookDto.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookDto : EntityDto +{ + public string Name { get; set; } = default!; +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteAppService.cs new file mode 100644 index 0000000000..c5b28e058b --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookLiteAppService : CrudAppService +{ + public BookLiteAppService(IRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteDto.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteDto.cs new file mode 100644 index 0000000000..7ab0408c85 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookLiteDto.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookLiteDto : EntityDto +{ + public string Name { get; set; } = default!; +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookObjectMapper.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookObjectMapper.cs new file mode 100644 index 0000000000..c398f9e25f --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookObjectMapper.cs @@ -0,0 +1,48 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookObjectMapper : + IObjectMapper, + IObjectMapper, + IObjectMapper, + ITransientDependency +{ + public const string Marker = "-mapped"; + + public BookDto Map(Book source) + { + return new BookDto { Id = source.Id, Name = source.Name + Marker }; + } + + public BookDto Map(Book source, BookDto destination) + { + destination.Id = source.Id; + destination.Name = source.Name + Marker; + return destination; + } + + BookLiteDto IObjectMapper.Map(Book source) + { + return new BookLiteDto { Id = source.Id, Name = source.Name + Marker }; + } + + BookLiteDto IObjectMapper.Map(Book source, BookLiteDto destination) + { + destination.Id = source.Id; + destination.Name = source.Name + Marker; + return destination; + } + + Book IObjectMapper.Map(BookDto source) + { + return new Book(source.Id, source.Name, 0); + } + + Book IObjectMapper.Map(BookDto source, Book destination) + { + destination.Name = source.Name; + return destination; + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookPolicyCheckedAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookPolicyCheckedAppService.cs new file mode 100644 index 0000000000..ee8e73db14 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookPolicyCheckedAppService.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookPolicyCheckedException : Exception +{ + +} + +public class BookPolicyCheckedAppService : CrudAppService +{ + public BookPolicyCheckedAppService(IRepository repository) + : base(repository) + { + + } + + protected override Task CheckGetPolicyAsync() + { + throw new BookPolicyCheckedException(); + } + + protected override Task CheckGetListPolicyAsync() + { + throw new BookPolicyCheckedException(); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookProjector.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookProjector.cs new file mode 100644 index 0000000000..fa292161fc --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookProjector.cs @@ -0,0 +1,18 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookProjector : IQueryProjector +{ + public const string Marker = "-projected"; + + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(book => new BookDto + { + Id = book.Id, + Name = book.Name + Marker + }); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookReadOnlyAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookReadOnlyAppService.cs new file mode 100644 index 0000000000..9a80d741fb --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookReadOnlyAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookReadOnlyAppService : ReadOnlyAppService +{ + public BookReadOnlyAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookRepository.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookRepository.cs new file mode 100644 index 0000000000..75da4e0da6 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookRepository.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +[ExposeServices( + typeof(IRepository), + typeof(IReadOnlyRepository), + typeof(IReadOnlyRepository))] +public class BookRepository : RepositoryBase, ISingletonDependency +{ + private readonly List _books = new(); + + public BookRepository() + : base("InMemory") + { + + } + + public override Task> GetQueryableAsync() + { + return Task.FromResult(_books.AsQueryable()); + } + + [Obsolete("Use GetQueryableAsync method.")] + protected override IQueryable GetQueryable() + { + return _books.AsQueryable(); + } + + public override Task GetAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default) + { + var book = _books.FirstOrDefault(x => x.Id == id); + if (book == null) + { + throw new EntityNotFoundException(typeof(Book), id); + } + + return Task.FromResult(book); + } + + public override Task FindAsync(Guid id, bool includeDetails = true, CancellationToken cancellationToken = default) + { + return Task.FromResult(_books.FirstOrDefault(x => x.Id == id)); + } + + public override Task FindAsync(Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) + { + return Task.FromResult(_books.AsQueryable().FirstOrDefault(predicate)); + } + + public override Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) + { + return Task.FromResult(_books.ToList()); + } + + public override Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) + { + return Task.FromResult(_books.AsQueryable().Where(predicate).ToList()); + } + + public override Task> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, bool includeDetails = false, CancellationToken cancellationToken = default) + { + return Task.FromResult(_books.Skip(skipCount).Take(maxResultCount).ToList()); + } + + public override Task GetCountAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult((long)_books.Count); + } + + public override Task InsertAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) + { + _books.Add(entity); + return Task.FromResult(entity); + } + + public override Task UpdateAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) + { + return Task.FromResult(entity); + } + + public override Task DeleteAsync(Book entity, bool autoSave = false, CancellationToken cancellationToken = default) + { + _books.Remove(entity); + return Task.CompletedTask; + } + + public override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) + { + _books.RemoveAll(new Predicate(predicate.Compile())); + return Task.CompletedTask; + } + + public override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return DeleteAsync(predicate); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructAppService.cs new file mode 100644 index 0000000000..34b4945b85 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookStructAppService : ReadOnlyAppService +{ + public BookStructAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructDto.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructDto.cs new file mode 100644 index 0000000000..f2690bb139 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public struct BookStructDto +{ + public Guid Id { get; set; } + + public string Name { get; set; } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructProjector.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructProjector.cs new file mode 100644 index 0000000000..4ae26a4b11 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookStructProjector.cs @@ -0,0 +1,16 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookStructProjector : IQueryProjector +{ + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(book => new BookStructDto + { + Id = book.Id, + Name = book.Name + }); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs new file mode 100644 index 0000000000..928c8fee66 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/BookWithoutProjectionAppService.cs @@ -0,0 +1,33 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class BookWithoutProjectionAppService : CrudAppService +{ + public const string Marker = "-not-projected"; + + protected override IQueryProjector? GetOutputDtoQueryProjector => null; + + protected override IQueryProjector? GetListOutputDtoQueryProjector => null; + + public BookWithoutProjectionAppService(IRepository repository) + : base(repository) + { + + } + + protected override Task MapToGetOutputDtoAsync(Book entity) + { + return Task.FromResult(new BookDto { Id = entity.Id, Name = entity.Name + Marker }); + } + + protected override Task> MapToGetListOutputDtosAsync(List entities) + { + return Task.FromResult(entities.ConvertAll(entity => new BookDto { Id = entity.Id, Name = entity.Name + Marker })); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/IBookNameSuffixProvider.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/IBookNameSuffixProvider.cs new file mode 100644 index 0000000000..747a93c15d --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/IBookNameSuffixProvider.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public interface IBookNameSuffixProvider +{ + Task GetAsync(); +} + +public class BookNameSuffixProvider : IBookNameSuffixProvider, ITransientDependency +{ + public async Task GetAsync() + { + await Task.Yield(); + + return BookAsyncProjectionAppService.Marker; + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs new file mode 100644 index 0000000000..425021fb03 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/QueryProjection/QueryProjection_Tests.cs @@ -0,0 +1,196 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; +using Xunit; + +namespace Volo.Abp.Application.Services.QueryProjection; + +public class QueryProjection_Tests : AbpDddApplicationTestBase +{ + private readonly Guid _bookId = Guid.NewGuid(); + + public QueryProjection_Tests() + { + var repository = GetRequiredService>(); + repository.InsertAsync(new Book(_bookId, "Hitchhiker's Guide", 42)).GetAwaiter().GetResult(); + } + + [Fact] + public void Should_Resolve_The_Projector_Independent_From_The_Class_Name() + { + ServiceProvider.GetService>() + .ShouldBeOfType(); + } + + [Fact] + public async Task Should_Project_On_The_Query_For_CrudAppService() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookProjector.Marker); + } + + [Fact] + public async Task Should_Project_On_The_Query_For_ReadOnlyAppService() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookProjector.Marker); + } + + [Fact] + public async Task Should_Throw_EntityNotFoundException_If_The_Projected_Entity_Does_Not_Exist() + { + var appService = GetRequiredService(); + + await Should.ThrowAsync(async () => await appService.GetAsync(Guid.NewGuid())); + } + + [Fact] + public async Task Should_Use_The_Object_Mapper_If_No_Projector_Was_Registered() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookObjectMapper.Marker); + } + + [Fact] + public async Task Should_Use_The_Entity_Based_Overrides_If_The_Projection_Was_Disabled() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookWithoutProjectionAppService.Marker); + } + + [Fact] + public async Task Should_Not_Use_The_Entity_Based_Overrides_While_Projecting() + { + var appService = GetRequiredService(); + + var dto = await appService.GetAsync(_bookId); + + dto.Name.ShouldEndWith(BookProjector.Marker); + dto.Name.ShouldNotContain(BookCustomizedAppService.Marker); + + var items = (await appService.GetListAsync(new PagedAndSortedResultRequestDto())).Items; + + items[0].Name.ShouldEndWith(BookProjector.Marker); + items[0].Name.ShouldNotContain(BookCustomizedAppService.Marker); + } + + [Fact] + public async Task Should_Use_The_Object_Mapper_If_The_Entity_Query_Can_Not_Be_Created() + { + var appService = GetRequiredService(); + + //GetAsync has no query to project, it falls back to GetEntityByIdAsync + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookObjectMapper.Marker); + + //GetListAsync always has a query, so it is still projected + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookProjector.Marker); + } + + [Fact] + public async Task Should_Use_The_Asynchronously_Created_Projection_Query() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookAsyncProjectionAppService.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookAsyncProjectionAppService.Marker); + } + + [Fact] + public async Task Should_Throw_EntityNotFoundException_From_An_Overridden_Projection_Query() + { + var appService = GetRequiredService(); + + await Should.ThrowAsync(async () => await appService.GetAsync(Guid.NewGuid())); + } + + [Fact] + public async Task Should_Throw_EntityNotFoundException_For_A_Value_Type_Dto() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldBe("Hitchhiker's Guide"); + + await Should.ThrowAsync(async () => await appService.GetAsync(Guid.NewGuid())); + } + + [Fact] + public async Task Should_Not_Project_On_The_Create_And_Update_Paths() + { + var appService = GetRequiredService(); + + var created = await appService.CreateAsync(new BookDto { Name = "New Book" }); + created.Name.ShouldEndWith(BookObjectMapper.Marker); + + var updated = await appService.UpdateAsync(created.Id, new BookDto { Name = "Updated Book" }); + updated.Name.ShouldEndWith(BookObjectMapper.Marker); + } + + [Fact] + public async Task Should_Use_A_Different_Projector_For_The_Get_And_The_List() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookDetailProjector.Marker); + (await appService.GetListAsync(new PagedAndSortedResultRequestDto())) + .Items[0].Name.ShouldEndWith(BookProjector.Marker); + } + + [Fact] + public async Task Should_Apply_Paging_And_Sorting_Before_Projecting() + { + var repository = GetRequiredService>(); + await repository.InsertAsync(new Book(Guid.NewGuid(), "A Book", 1)); + await repository.InsertAsync(new Book(Guid.NewGuid(), "Z Book", 2)); + + var appService = GetRequiredService(); + + var firstPage = await appService.GetListAsync( + new PagedAndSortedResultRequestDto { MaxResultCount = 1, Sorting = "Name" }); + + firstPage.TotalCount.ShouldBe(3); + firstPage.Items.Count.ShouldBe(1); + firstPage.Items[0].Name.ShouldBe("A Book" + BookProjector.Marker); + + var secondPage = await appService.GetListAsync( + new PagedAndSortedResultRequestDto { MaxResultCount = 1, SkipCount = 1, Sorting = "Name" }); + + secondPage.Items[0].Name.ShouldBe("Hitchhiker's Guide" + BookProjector.Marker); + } + + [Fact] + public async Task Should_Project_A_Single_Entity_From_An_AbstractKey_Application_Service() + { + var appService = GetRequiredService(); + + (await appService.GetAsync(_bookId)).Name.ShouldEndWith(BookProjector.Marker); + } + + [Fact] + public async Task Should_Check_The_Policies_While_Projecting() + { + var appService = GetRequiredService(); + + await Should.ThrowAsync(async () => await appService.GetAsync(_bookId)); + await Should.ThrowAsync(async () => + await appService.GetListAsync(new PagedAndSortedResultRequestDto())); + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index 27992ed124..fae245bc3a 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; using Volo.Abp.Data; using Volo.Abp.Domain.Repositories; +using Volo.Abp.EntityFrameworkCore.Applications; using Volo.Abp.EntityFrameworkCore.Domain; using Volo.Abp.EntityFrameworkCore.Sqlite; using Volo.Abp.EntityFrameworkCore.TestApp.FifthContext; @@ -92,6 +93,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule options.Configure(abpDbContextConfigurationContext => { abpDbContextConfigurationContext.UseSqlite().AddAbpDbContextOptionsExtension(); + abpDbContextConfigurationContext.DbContextOptions.AddInterceptors(new SqlCommandCapture()); }); }); } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionAppService.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionAppService.cs new file mode 100644 index 0000000000..cc6176cddd --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionAppService.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class EntityWithIntPkProjectionAppService : ReadOnlyAppService +{ + public EntityWithIntPkProjectionAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionDto.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionDto.cs new file mode 100644 index 0000000000..f6bbebe850 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjectionDto.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class EntityWithIntPkProjectionDto : EntityDto +{ + public string Name { get; set; } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjector.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjector.cs new file mode 100644 index 0000000000..5e97ceca51 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/EntityWithIntPkProjector.cs @@ -0,0 +1,17 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class EntityWithIntPkProjector : IQueryProjector +{ + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(entity => new EntityWithIntPkProjectionDto + { + Id = entity.Id, + Name = entity.Name + }); + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionAppService.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionAppService.cs new file mode 100644 index 0000000000..fa51e9407f --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionAppService.cs @@ -0,0 +1,15 @@ +using System; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class PersonProjectionAppService : ReadOnlyAppService +{ + public PersonProjectionAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionDto.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionDto.cs new file mode 100644 index 0000000000..57eef129d0 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjectionDto.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class PersonProjectionDto : EntityDto +{ + public string Name { get; set; } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjector.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjector.cs new file mode 100644 index 0000000000..d9abce7cd8 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonProjector.cs @@ -0,0 +1,17 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class PersonProjector : IQueryProjector +{ + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(person => new PersonProjectionDto + { + Id = person.Id, + Name = person.Name + }); + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityAppService.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityAppService.cs new file mode 100644 index 0000000000..f8a8ba01e5 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityAppService.cs @@ -0,0 +1,51 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +//City is another aggregate root, so Person has no City navigation property to project. +public class PersonWithCityAppService : ReadOnlyAppService +{ + private readonly IReadOnlyRepository _cityRepository; + + public PersonWithCityAppService( + IReadOnlyRepository repository, + IReadOnlyRepository cityRepository) + : base(repository) + { + _cityRepository = cityRepository; + } + + protected override async Task?> CreateGetOutputDtoQueryOrNullAsync(Guid id) + { + var people = await CreateEntityQueryOrNullAsync(id); + + return people == null ? null : await JoinCitiesAsync(people); + } + + protected override async Task?> CreateGetListOutputDtoQueryOrNullAsync(IQueryable query) + { + return await JoinCitiesAsync(query); + } + + //left join, an inner join would drop the people without a city and break the total count + private async Task> JoinCitiesAsync(IQueryable people) + { + var cities = await _cityRepository.GetQueryableAsync(); + + return from person in people + join city in cities on person.CityId equals city.Id into personCities + from personCity in personCities.DefaultIfEmpty() + select new PersonWithCityDto + { + Id = person.Id, + Name = person.Name, + CityName = personCity != null ? personCity.Name : null + }; + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs new file mode 100644 index 0000000000..dac278c7b6 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/PersonWithCityDto.cs @@ -0,0 +1,12 @@ +#nullable enable +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class PersonWithCityDto : EntityDto +{ + public string Name { get; set; } + + public string? CityName { get; set; } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs new file mode 100644 index 0000000000..d4410c15e7 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/QueryProjection_Tests.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; +using Volo.Abp.TestApp; +using Volo.Abp.TestApp.Application; +using Volo.Abp.Threading; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class QueryProjection_Tests : EntityFrameworkCoreTestBase +{ + private readonly PersonProjectionAppService _personProjectionAppService; + + public QueryProjection_Tests() + { + _personProjectionAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_A_Projected_Entity() + { + var dto = await _personProjectionAppService.GetAsync(TestDataBuilder.UserDouglasId); + + dto.Id.ShouldBe(TestDataBuilder.UserDouglasId); + dto.Name.ShouldBe("Douglas"); + } + + [Fact] + public async Task Should_Throw_EntityNotFoundException_For_A_Missing_Entity() + { + (await Should.ThrowAsync>( + async () => await _personProjectionAppService.GetAsync(Guid.NewGuid())) + ).EntityType.ShouldBe(typeof(Person)); + } + + [Fact] + public async Task Should_Get_A_Projected_List() + { + var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.Count.ShouldBe((int)result.TotalCount); + result.Items.ShouldContain(x => x.Name == "Douglas"); + } + + [Fact] + public async Task Should_Get_A_Projected_Entity_With_A_Non_Guid_Key() + { + var appService = GetRequiredService(); + var entity = await WithUnitOfWorkAsync( + async () => await GetRequiredService>().FirstAsync()); + + (await appService.GetAsync(entity.Id)).Name.ShouldBe(entity.Name); + } + + [Fact] + public async Task Should_Apply_The_Soft_Delete_Filter_While_Projecting() + { + var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + + result.Items.ShouldNotContain(x => x.Id == TestDataBuilder.UserJohnDeletedId); + } + + [Fact] + public async Task Should_Only_Select_The_Projected_Columns() + { + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + var projector = GetRequiredService>(); + + var sql = projector.ProjectTo(await repository.GetQueryableAsync()).ToQueryString(); + + sql.ShouldContain("\"Name\""); + sql.ShouldNotContain("\"Birthday\""); + sql.ShouldNotContain("\"ExtraProperties\""); + + //the data filters are still a part of the query + sql.ShouldContain("Is_Deleted"); + }); + } + + [Fact] + public async Task Should_Join_Another_Aggregate_While_Projecting() + { + var appService = GetRequiredService(); + + var dto = await appService.GetAsync(TestDataBuilder.UserDouglasId); + dto.CityName.ShouldBe("London"); + + var result = await appService.GetListAsync(new PagedAndSortedResultRequestDto()); + result.Items.ShouldContain(x => x.CityName == "London"); + } + + [Fact] + public async Task Should_Keep_The_Total_Count_While_Joining_Another_Aggregate() + { + await WithUnitOfWorkAsync(async () => + { + await GetRequiredService>() + .InsertAsync(new Person(Guid.NewGuid(), "PersonWithoutCity", 30), autoSave: true); + }); + + var result = await GetRequiredService() + .GetListAsync(new PagedAndSortedResultRequestDto()); + + result.Items.Count.ShouldBe((int)result.TotalCount); + result.Items.ShouldContain(x => x.Name == "PersonWithoutCity" && x.CityName == null); + } + + [Fact] + public async Task Should_Execute_The_Projected_Query_From_The_Application_Service() + { + System.Collections.Concurrent.ConcurrentQueue commands; + using (SqlCommandCapture.Begin(out commands)) + { + await GetRequiredService() + .GetListAsync(new PagedAndSortedResultRequestDto()); + } + + //the application service must run the projection itself, not materialize the entities first + var selects = commands.Where(x => x.Contains("FROM \"People\"") && !x.Contains("COUNT")).ToList(); + + var select = selects.ShouldHaveSingleItem(); + select.ShouldContain("\"Name\""); + select.ShouldNotContain("\"Birthday\""); + select.ShouldNotContain("\"ExtraProperties\""); + } + + [Fact] + public async Task Should_Apply_The_Multi_Tenancy_Filter_While_Projecting() + { + var result = await GetRequiredService() + .GetListAsync(new PagedAndSortedResultRequestDto()); + + result.Items.ShouldNotContain(x => x.Name.StartsWith(TestDataBuilder.TenantId1.ToString())); + } + + [Fact] + public async Task Should_Use_The_Ambient_Cancellation_Token_While_Projecting() + { + var cancellationTokenProvider = GetRequiredService(); + var appService = GetRequiredService(); + + //the entity path passes it through Repository.GetAsync, so the projected path must not regress + using (cancellationTokenProvider.Use(new CancellationToken(canceled: true))) + { + await Should.ThrowAsync(async () => + await appService.GetAsync(TestDataBuilder.UserDouglasId)); + } + } + + [Fact] + public async Task Should_Execute_The_Projected_Query_From_The_Application_Service_For_A_Single_Entity() + { + System.Collections.Concurrent.ConcurrentQueue commands; + using (SqlCommandCapture.Begin(out commands)) + { + await GetRequiredService().GetAsync(TestDataBuilder.UserDouglasId); + } + + var selects = commands.Where(x => x.Contains("FROM \"People\"")).ToList(); + + var select = selects.ShouldHaveSingleItem(); + select.ShouldContain("\"Name\""); + select.ShouldNotContain("\"Birthday\""); + select.ShouldNotContain("\"ExtraProperties\""); + } + + [Fact] + public async Task Should_Apply_The_Data_Filters_Of_The_Joined_Aggregate() + { + var cityRepository = GetRequiredService>(); + + await WithUnitOfWorkAsync(async () => + { + var london = await cityRepository.GetAsync(TestDataBuilder.LondonCityId); + await cityRepository.DeleteAsync(london, autoSave: true); + }); + + var result = await GetRequiredService() + .GetListAsync(new PagedAndSortedResultRequestDto()); + + //the soft deleted city is filtered out, the left join still keeps the person + result.Items.ShouldContain(x => x.Name == "Douglas" && x.CityName == null); + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/SqlCommandCapture.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/SqlCommandCapture.cs new file mode 100644 index 0000000000..7f70e562e5 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Applications/SqlCommandCapture.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Volo.Abp.EntityFrameworkCore.Applications; + +public class SqlCommandCapture : DbCommandInterceptor +{ + private static readonly AsyncLocal> Commands = new(); + + public static IDisposable Begin(out ConcurrentQueue commands) + { + commands = new ConcurrentQueue(); + Commands.Value = commands; + return new DisposeAction(() => Commands.Value = null); + } + + public override InterceptionResult ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult result) + { + Commands.Value?.Enqueue(command.CommandText); + return base.ReaderExecuting(command, eventData, result); + } + + public override ValueTask> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + Commands.Value?.Enqueue(command.CommandText); + return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.abppkg b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.abppkg new file mode 100644 index 0000000000..a686451fbc --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj new file mode 100644 index 0000000000..f3001bd8a8 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo.Abp.Http.FluentValidation.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + net10.0 + enable + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestBase.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestBase.cs new file mode 100644 index 0000000000..be25f2b813 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestBase.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Http.Modeling; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; + +namespace Volo.Abp.Http.FluentValidation; + +public abstract class AbpHttpFluentValidationTestBase : AbpIntegratedTest + where TStartupModule : IAbpModule +{ + protected virtual async Task GetPropertyAsync(string propertyName) + { + var typeModel = await CreateTypeModelAsync(typeof(TDto)); + return typeModel.Properties!.Single(x => x.Name == propertyName); + } + + protected virtual async Task CreateTypeModelAsync(Type type) + { + var typeModel = TypeApiDescriptionModel.Create(type); + var contributors = ServiceProvider.GetServices().ToArray(); + + var propertyInfos = type + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.DeclaringType == type) + .ToDictionary(p => p.Name, p => p); + + foreach (var propertyModel in typeModel.Properties!) + { + var context = new PropertyApiDescriptionModelContributionContext(propertyModel, propertyInfos[propertyModel.Name], type); + foreach (var contributor in contributors) + { + await contributor.ContributeAsync(context); + } + } + + return typeModel; + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestModule.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestModule.cs new file mode 100644 index 0000000000..93f54d5afa --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/AbpHttpFluentValidationTestModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Http.FluentValidation; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpHttpFluentValidationModule) + )] +public class AbpHttpFluentValidationTestModule : AbpModule +{ +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs new file mode 100644 index 0000000000..699729689f --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_Tests.cs @@ -0,0 +1,358 @@ +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Autofac; +using Volo.Abp.Http.FluentValidation.TestObjects; +using Volo.Abp.Http.Modeling; +using Volo.Abp.Localization; +using Xunit; + +namespace Volo.Abp.Http.FluentValidation; + +public class FluentValidationApiDescription_Tests : AbpHttpFluentValidationTestBase +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [Fact] + public void Should_Register_The_Contributor() + { + ServiceProvider + .GetServices() + .ShouldContain(x => x is FluentValidationPropertyApiDescriptionModelContributor); + } + + [Fact] + public async Task Should_Map_Required_Rules() + { + (await GetPropertyAsync(nameof(ConstraintTestDto.NotEmptyValue))).IsRequired.ShouldBeTrue(); + (await GetPropertyAsync(nameof(ConstraintTestDto.NotNullValue))).IsRequired.ShouldBeTrue(); + (await GetPropertyAsync(nameof(ConstraintTestDto.UnconstrainedValue))).IsRequired.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Map_Length_Rules() + { + var length = await GetPropertyAsync(nameof(ConstraintTestDto.LengthValue)); + length.MinLength.ShouldBe(3); + length.MaxLength.ShouldBe(10); + + var minimumLength = await GetPropertyAsync(nameof(ConstraintTestDto.MinimumLengthValue)); + minimumLength.MinLength.ShouldBe(4); + minimumLength.MaxLength.ShouldBeNull(); + + var maximumLength = await GetPropertyAsync(nameof(ConstraintTestDto.MaximumLengthValue)); + maximumLength.MinLength.ShouldBeNull(); + maximumLength.MaxLength.ShouldBe(12); + } + + [Fact] + public async Task Should_Not_Map_Zero_Length_Rules() + { + // A Func bound is reported as a zero too, so a zero can not be published safely. + (await GetPropertyAsync(nameof(ConstraintTestDto.EmptyOnlyValue))).MaxLength.ShouldBeNull(); + (await GetPropertyAsync(nameof(ConstraintTestDto.ZeroLengthValue))).MaxLength.ShouldBeNull(); + + var dynamicLength = await GetPropertyAsync(nameof(ConstraintTestDto.DynamicLengthValue)); + dynamicLength.MinLength.ShouldBeNull(); + dynamicLength.MaxLength.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_A_Comparison_Bound_That_Is_Not_A_Number() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.DateComparisonValue)); + + property.Minimum.ShouldBeNull(); + property.MinimumIsExclusive.ShouldBeNull(); + } + + [Fact] + public async Task Should_Map_A_Bound_Written_In_The_Exponent_Notation() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.SmallExponentValue)); + + property.Minimum.ShouldBe("1E-20"); + } + + [Fact] + public async Task Should_Map_A_Bound_Below_The_Decimal_Range() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.UnderflowExponentValue)); + + property.Minimum.ShouldBe("1E-30"); + } + + [Fact] + public async Task Should_Map_A_Comparison_On_A_Native_Integer() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.NativeIntegerValue)); + + property.Minimum.ShouldBe("5"); + } + + [Fact] + public async Task Should_Not_Map_A_Between_Rule_That_Reads_As_An_Empty_Interval() + { + // The rule carries its own comparer, so its bounds mean the opposite of what they say. + var property = await GetPropertyAsync(nameof(ConstraintTestDto.ReversedBetweenValue)); + + property.Minimum.ShouldBeNull(); + property.Maximum.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_A_Comparison_On_A_Property_That_Is_Not_A_Number() + { + // The server compares two strings ordinally, so a numeric bound would say something else. + var property = await GetPropertyAsync(nameof(ConstraintTestDto.StringComparisonValue)); + + property.Minimum.ShouldBeNull(); + property.MinimumIsExclusive.ShouldBeNull(); + } + + [Fact] + public async Task Should_Map_Regular_Expression_Rules() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.RegexValue)); + + property.Regex.ShouldBe("^[a-z]+$"); + } + + [Fact] + public async Task Should_Map_Inclusive_Comparison_Rules() + { + var minimum = await GetPropertyAsync(nameof(ConstraintTestDto.InclusiveMinimumValue)); + minimum.Minimum.ShouldBe("5"); + minimum.MinimumIsExclusive.ShouldBe(false); + + var maximum = await GetPropertyAsync(nameof(ConstraintTestDto.InclusiveMaximumValue)); + maximum.Maximum.ShouldBe("50"); + maximum.MaximumIsExclusive.ShouldBe(false); + + var between = await GetPropertyAsync(nameof(ConstraintTestDto.InclusiveBetweenValue)); + between.Minimum.ShouldBe("1"); + between.Maximum.ShouldBe("10"); + between.MinimumIsExclusive.ShouldBe(false); + between.MaximumIsExclusive.ShouldBe(false); + } + + [Fact] + public async Task Should_Map_Exclusive_Comparison_Rules() + { + var minimum = await GetPropertyAsync(nameof(ConstraintTestDto.ExclusiveMinimumValue)); + minimum.Minimum.ShouldBe("5"); + minimum.MinimumIsExclusive.ShouldBe(true); + + var maximum = await GetPropertyAsync(nameof(ConstraintTestDto.ExclusiveMaximumValue)); + maximum.Maximum.ShouldBe("50"); + maximum.MaximumIsExclusive.ShouldBe(true); + + var between = await GetPropertyAsync(nameof(ConstraintTestDto.ExclusiveBetweenValue)); + between.Minimum.ShouldBe("1"); + between.Maximum.ShouldBe("10"); + between.MinimumIsExclusive.ShouldBe(true); + between.MaximumIsExclusive.ShouldBe(true); + } + + [Fact] + public async Task Should_Not_Map_Comparison_Against_Another_Property() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.ComparedToOtherPropertyValue)); + + property.Minimum.ShouldBeNull(); + property.Maximum.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_RuleForEach_Rules_To_The_Collection_Property() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.CollectionValue)); + + property.IsRequired.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Not_Map_Conditional_Rules() + { + (await GetPropertyAsync(nameof(ConditionalTestDto.ChainedConditionValue))).IsRequired.ShouldBeFalse(); + (await GetPropertyAsync(nameof(ConditionalTestDto.UnlessConditionValue))).IsRequired.ShouldBeFalse(); + (await GetPropertyAsync(nameof(ConditionalTestDto.AsyncConditionValue))).IsRequired.ShouldBeFalse(); + (await GetPropertyAsync(nameof(ConditionalTestDto.UnconditionalValue))).IsRequired.ShouldBeTrue(); + + var blockCondition = await GetPropertyAsync(nameof(ConditionalTestDto.BlockConditionValue)); + blockCondition.IsRequired.ShouldBeFalse(); + blockCondition.MaxLength.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_Rules_Of_Named_Rule_Sets() + { + var namedRuleSet = await GetPropertyAsync(nameof(RuleSetTestDto.NamedRuleSetValue)); + namedRuleSet.IsRequired.ShouldBeFalse(); + namedRuleSet.MinLength.ShouldBeNull(); + + var defaultRuleSet = await GetPropertyAsync(nameof(RuleSetTestDto.DefaultRuleSetValue)); + defaultRuleSet.IsRequired.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Keep_The_Stricter_Bound_When_Merging_With_Data_Annotations() + { + var length = await GetPropertyAsync(nameof(DataAnnotationTestDto.MergedLengthValue)); + length.MinLength.ShouldBe(5); + length.MaxLength.ShouldBe(10); + + var range = await GetPropertyAsync(nameof(DataAnnotationTestDto.MergedRangeValue)); + range.Minimum.ShouldBe("10"); + range.Maximum.ShouldBe("90"); + range.MinimumIsExclusive.ShouldBe(false); + range.MaximumIsExclusive.ShouldBe(false); + } + + [Fact] + public async Task Should_Read_Exclusive_Bounds_From_The_Range_Attribute() + { + var property = await GetPropertyAsync(nameof(DataAnnotationTestDto.ExclusiveAttributeValue)); + + property.Minimum.ShouldBe("1"); + property.Maximum.ShouldBe("100"); + property.MinimumIsExclusive.ShouldBe(true); + property.MaximumIsExclusive.ShouldBe(true); + } + + [Fact] + public async Task Should_Keep_The_Exclusivity_Of_The_Winning_Bound() + { + // The attribute bounds are stricter, so the exclusive flags must not leak onto them. + var looser = await GetPropertyAsync(nameof(DataAnnotationTestDto.LooserFluentBoundValue)); + looser.Minimum.ShouldBe("0"); + looser.Maximum.ShouldBe("100"); + looser.MinimumIsExclusive.ShouldBe(false); + looser.MaximumIsExclusive.ShouldBe(false); + + // On an equal bound the exclusive rule is the stricter one and wins. + var same = await GetPropertyAsync(nameof(DataAnnotationTestDto.SameBoundValue)); + same.Minimum.ShouldBe("10"); + same.Maximum.ShouldBe("90"); + same.MinimumIsExclusive.ShouldBe(true); + same.MaximumIsExclusive.ShouldBe(true); + } + + [Fact] + public async Task Should_Compare_Bounds_Beyond_The_Double_Precision_Exactly() + { + // Both bounds collapse to the same double, so only an exact comparison keeps the + // stricter attribute bound instead of replacing it with the looser rule. + var property = await GetPropertyAsync(nameof(DataAnnotationTestDto.HighPrecisionValue)); + + property.Minimum.ShouldBe("9007199254740993"); + property.Maximum.ShouldBe("18446744073709551615"); + } + + [Fact] + public async Task Should_Keep_The_Attribute_Regular_Expression() + { + var property = await GetPropertyAsync(nameof(DataAnnotationTestDto.AttributeRegexValue)); + + property.Regex.ShouldBe("^attribute$"); + } + + [Fact] + public async Task Should_Ignore_Types_Without_A_Validator() + { + var property = await GetPropertyAsync(nameof(UnvalidatedTestDto.Value)); + + property.IsRequired.ShouldBeFalse(); + property.MinLength.ShouldBeNull(); + property.MaxLength.ShouldBeNull(); + } + + [Fact] + public async Task Should_Ignore_Open_Generic_Types() + { + var typeModel = await CreateTypeModelAsync(typeof(GenericTestDto<>)); + var property = typeModel.Properties!.Single(x => x.Name == nameof(GenericTestDto.Name)); + + property.IsRequired.ShouldBeFalse(); + property.MinLength.ShouldBeNull(); + property.MaxLength.ShouldBeNull(); + property.Regex.ShouldBeNull(); + } + + [Fact] + public async Task Should_Merge_Range_Attribute_Bounds_Under_Any_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var property = await GetPropertyAsync(nameof(CultureTestDto.DecimalRangeValue)); + + property.Minimum.ShouldBe("2"); + property.Maximum.ShouldBe("9.5"); + + var typed = await GetPropertyAsync(nameof(CultureTestDto.TypedDecimalRangeValue)); + typed.Minimum.ShouldBe("2"); + typed.Maximum.ShouldBe("9.5"); + + // A bound outside the decimal range still loses to the stricter rule. + var exponent = await GetPropertyAsync(nameof(CultureTestDto.ExponentRangeValue)); + exponent.Minimum.ShouldBe("2"); + exponent.Maximum.ShouldBe("1E+30"); + } + } + + [Fact] + public async Task Should_Map_Rules_With_An_Overridden_Property_Name() + { + var property = await GetPropertyAsync(nameof(PropertyNameTestDto.RenamedValue)); + + property.IsRequired.ShouldBeTrue(); + property.MaxLength.ShouldBe(20); + } + + [Fact] + public async Task Should_Not_Map_Rules_Of_A_Nested_Object_To_A_Property_Of_The_Same_Name() + { + var property = await GetPropertyAsync(nameof(PropertyNameTestDto.City)); + + property.IsRequired.ShouldBeFalse(); + property.MaxLength.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_Rules_Of_A_Nested_Object_To_The_Nested_Type() + { + // The model of the nested type is shared by every DTO that uses it, so a rule declared + // by the validator of one of them can not be attributed to it. + var property = await GetPropertyAsync(nameof(PropertyNameTestAddress.City)); + + property.IsRequired.ShouldBeFalse(); + property.MaxLength.ShouldBeNull(); + } + + [Fact] + public async Task Should_Not_Map_Rules_Of_A_Derived_Validator_To_A_Base_Property() + { + // Each type describes only the properties it declares, and the base type is described + // with its own validator, which does not exist here. + var inherited = await GetPropertyAsync(nameof(InheritanceTestBaseDto.InheritedValue)); + inherited.IsRequired.ShouldBeFalse(); + inherited.MaxLength.ShouldBeNull(); + + var own = await GetPropertyAsync(nameof(InheritanceTestDto.OwnValue)); + own.IsRequired.ShouldBeTrue(); + } + + [Fact] + public async Task Should_Keep_A_Bound_That_Is_Not_A_Number() + { + var property = await GetPropertyAsync(nameof(CultureTestDto.DateRangeValue)); + + property.Minimum.ShouldBe("2020-01-01"); + property.Maximum.ShouldBe("2030-01-01"); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_WithoutAutofac_Tests.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_WithoutAutofac_Tests.cs new file mode 100644 index 0000000000..f9fc111fac --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/FluentValidationApiDescription_WithoutAutofac_Tests.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Http.FluentValidation.TestObjects; +using Volo.Abp.Modularity; +using Xunit; + +namespace Volo.Abp.Http.FluentValidation; + +public class FluentValidationApiDescription_WithoutAutofac_Tests + : AbpHttpFluentValidationTestBase +{ + [DependsOn(typeof(AbpHttpFluentValidationModule))] + public class TestModule : AbpModule + { + } + + [Fact] + public async Task Should_Contribute_Without_Autofac() + { + var property = await GetPropertyAsync(nameof(ConstraintTestDto.LengthValue)); + + property.MinLength.ShouldBe(3); + property.MaxLength.ShouldBe(10); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConditionalTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConditionalTestDto.cs new file mode 100644 index 0000000000..59a27e0c84 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConditionalTestDto.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class ConditionalTestDto +{ + public bool Enabled { get; set; } + + public string? ChainedConditionValue { get; set; } + + public string? BlockConditionValue { get; set; } + + public string? UnlessConditionValue { get; set; } + + public string? AsyncConditionValue { get; set; } + + public string? UnconditionalValue { get; set; } +} + +public class ConditionalTestDtoValidator : AbstractValidator +{ + public ConditionalTestDtoValidator() + { + RuleFor(x => x.ChainedConditionValue).NotEmpty().When(x => x.Enabled); + + When(x => x.Enabled, () => + { + RuleFor(x => x.BlockConditionValue).NotEmpty().MaximumLength(32); + }); + + Unless(x => x.Enabled, () => + { + RuleFor(x => x.UnlessConditionValue).NotEmpty(); + }); + + RuleFor(x => x.AsyncConditionValue).NotEmpty().WhenAsync((x, _) => Task.FromResult(x.Enabled)); + + RuleFor(x => x.UnconditionalValue).NotEmpty(); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConstraintTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConstraintTestDto.cs new file mode 100644 index 0000000000..2a93cbd79e --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/ConstraintTestDto.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class ConstraintTestDto +{ + public string? NotEmptyValue { get; set; } + + public string? NotNullValue { get; set; } + + public string? LengthValue { get; set; } + + public string? MinimumLengthValue { get; set; } + + public string? MaximumLengthValue { get; set; } + + public string? RegexValue { get; set; } + + public int InclusiveMinimumValue { get; set; } + + public int InclusiveMaximumValue { get; set; } + + public int ExclusiveMinimumValue { get; set; } + + public int ExclusiveMaximumValue { get; set; } + + public int InclusiveBetweenValue { get; set; } + + public int ExclusiveBetweenValue { get; set; } + + public int ComparedToOtherPropertyValue { get; set; } + + public List CollectionValue { get; set; } = new List(); + + public string? EmptyOnlyValue { get; set; } + + public string? ZeroLengthValue { get; set; } + + public string? DynamicLengthValue { get; set; } + + public DateTime DateComparisonValue { get; set; } + + public double SmallExponentValue { get; set; } + + public double UnderflowExponentValue { get; set; } + + public string? StringComparisonValue { get; set; } + + public nint NativeIntegerValue { get; set; } + + public int ReversedBetweenValue { get; set; } + + public string? UnconstrainedValue { get; set; } +} + +public class ReversedComparer : IComparer +{ + public int Compare(int x, int y) + { + return y.CompareTo(x); + } +} + +public class ConstraintTestDtoValidator : AbstractValidator +{ + public ConstraintTestDtoValidator() + { + RuleFor(x => x.NotEmptyValue).NotEmpty(); + RuleFor(x => x.NotNullValue).NotNull(); + RuleFor(x => x.LengthValue).Length(3, 10); + RuleFor(x => x.MinimumLengthValue).MinimumLength(4); + RuleFor(x => x.MaximumLengthValue).MaximumLength(12); + RuleFor(x => x.RegexValue).Matches("^[a-z]+$"); + RuleFor(x => x.InclusiveMinimumValue).GreaterThanOrEqualTo(5); + RuleFor(x => x.InclusiveMaximumValue).LessThanOrEqualTo(50); + RuleFor(x => x.ExclusiveMinimumValue).GreaterThan(5); + RuleFor(x => x.ExclusiveMaximumValue).LessThan(50); + RuleFor(x => x.InclusiveBetweenValue).InclusiveBetween(1, 10); + RuleFor(x => x.ExclusiveBetweenValue).ExclusiveBetween(1, 10); + RuleFor(x => x.ComparedToOtherPropertyValue).GreaterThanOrEqualTo(x => x.InclusiveMinimumValue); + RuleForEach(x => x.CollectionValue).NotEmpty(); + RuleFor(x => x.EmptyOnlyValue).MaximumLength(0); + RuleFor(x => x.ZeroLengthValue).Length(0, 0); + RuleFor(x => x.DateComparisonValue).GreaterThanOrEqualTo(new DateTime(2020, 1, 1)); + RuleFor(x => x.DynamicLengthValue).Length(_ => 2, _ => 8); + RuleFor(x => x.SmallExponentValue).GreaterThanOrEqualTo(1e-20); + RuleFor(x => x.UnderflowExponentValue).GreaterThanOrEqualTo(1e-30); + RuleFor(x => x.StringComparisonValue).GreaterThan("10"); + RuleFor(x => x.NativeIntegerValue).GreaterThanOrEqualTo((nint)5); + RuleFor(x => x.ReversedBetweenValue).InclusiveBetween(10, 1, new ReversedComparer()); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs new file mode 100644 index 0000000000..13aca5cfeb --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/CultureTestDto.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel.DataAnnotations; +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class CultureTestDto +{ + [Range(1.5, 9.5)] + public double DecimalRangeValue { get; set; } + + [Range(typeof(decimal), "1,5", "9,5")] + public decimal TypedDecimalRangeValue { get; set; } + + [Range(typeof(double), "1e-30", "1e30", ParseLimitsInInvariantCulture = true)] + public double ExponentRangeValue { get; set; } + + [Range(typeof(DateTime), "2020-01-01", "2030-01-01")] + public DateTime DateRangeValue { get; set; } +} + +public class CultureTestDtoValidator : AbstractValidator +{ + public CultureTestDtoValidator() + { + RuleFor(x => x.DecimalRangeValue).GreaterThanOrEqualTo(2.0); + RuleFor(x => x.TypedDecimalRangeValue).GreaterThanOrEqualTo(2m); + RuleFor(x => x.ExponentRangeValue).GreaterThanOrEqualTo(2d); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/DataAnnotationTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/DataAnnotationTestDto.cs new file mode 100644 index 0000000000..1220d65a24 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/DataAnnotationTestDto.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class DataAnnotationTestDto +{ + [MinLength(2)] + [MaxLength(50)] + public string? MergedLengthValue { get; set; } + + [RegularExpression("^attribute$")] + public string? AttributeRegexValue { get; set; } + + [Range(0, 100)] + public int MergedRangeValue { get; set; } + + [Range(0, 100)] + public int LooserFluentBoundValue { get; set; } + + [Range(10, 90)] + public int SameBoundValue { get; set; } + + [Range(1, 100, MinimumIsExclusive = true, MaximumIsExclusive = true)] + public int ExclusiveAttributeValue { get; set; } + + [Range(typeof(ulong), "9007199254740993", "18446744073709551615", ParseLimitsInInvariantCulture = true)] + public ulong HighPrecisionValue { get; set; } +} + +public class DataAnnotationTestDtoValidator : AbstractValidator +{ + public DataAnnotationTestDtoValidator() + { + RuleFor(x => x.MergedLengthValue).Length(5, 10); + RuleFor(x => x.AttributeRegexValue).Matches("^fluent$"); + RuleFor(x => x.MergedRangeValue).GreaterThanOrEqualTo(10).LessThanOrEqualTo(90); + RuleFor(x => x.LooserFluentBoundValue).GreaterThan(-5).LessThan(500); + RuleFor(x => x.SameBoundValue).GreaterThan(10).LessThan(90); + RuleFor(x => x.HighPrecisionValue).GreaterThanOrEqualTo(9_007_199_254_740_992UL); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/GenericTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/GenericTestDto.cs new file mode 100644 index 0000000000..64f5145376 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/GenericTestDto.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class GenericTestDto +{ + public T? Value { get; set; } + + public string? Name { get; set; } +} + +public class GenericTestDtoValidator : AbstractValidator> +{ + public GenericTestDtoValidator() + { + RuleFor(x => x.Name).NotEmpty().Length(3, 9).Matches("^[a-z]+$"); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/InheritanceTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/InheritanceTestDto.cs new file mode 100644 index 0000000000..13f78223d5 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/InheritanceTestDto.cs @@ -0,0 +1,22 @@ +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class InheritanceTestBaseDto +{ + public string? InheritedValue { get; set; } +} + +public class InheritanceTestDto : InheritanceTestBaseDto +{ + public string? OwnValue { get; set; } +} + +public class InheritanceTestDtoValidator : AbstractValidator +{ + public InheritanceTestDtoValidator() + { + RuleFor(x => x.InheritedValue).NotEmpty().MaximumLength(15); + RuleFor(x => x.OwnValue).NotEmpty(); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/PropertyNameTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/PropertyNameTestDto.cs new file mode 100644 index 0000000000..390d186e00 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/PropertyNameTestDto.cs @@ -0,0 +1,26 @@ +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class PropertyNameTestDto +{ + public string? RenamedValue { get; set; } + + public string? City { get; set; } + + public PropertyNameTestAddress Address { get; set; } = new PropertyNameTestAddress(); +} + +public class PropertyNameTestAddress +{ + public string? City { get; set; } +} + +public class PropertyNameTestDtoValidator : AbstractValidator +{ + public PropertyNameTestDtoValidator() + { + RuleFor(x => x.RenamedValue).NotEmpty().MaximumLength(20).OverridePropertyName("displayed_name"); + RuleFor(x => x.Address.City).NotEmpty().MaximumLength(30); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/RuleSetTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/RuleSetTestDto.cs new file mode 100644 index 0000000000..e5867996a6 --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/RuleSetTestDto.cs @@ -0,0 +1,26 @@ +using FluentValidation; + +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class RuleSetTestDto +{ + public string? NamedRuleSetValue { get; set; } + + public string? DefaultRuleSetValue { get; set; } +} + +public class RuleSetTestDtoValidator : AbstractValidator +{ + public RuleSetTestDtoValidator() + { + RuleSet("Create", () => + { + RuleFor(x => x.NamedRuleSetValue).NotEmpty().MinimumLength(4); + }); + + RuleSet("default", () => + { + RuleFor(x => x.DefaultRuleSetValue).NotEmpty(); + }); + } +} diff --git a/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/UnvalidatedTestDto.cs b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/UnvalidatedTestDto.cs new file mode 100644 index 0000000000..c82257ecbd --- /dev/null +++ b/framework/test/Volo.Abp.Http.FluentValidation.Tests/Volo/Abp/Http/FluentValidation/TestObjects/UnvalidatedTestDto.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Http.FluentValidation.TestObjects; + +public class UnvalidatedTestDto +{ + public string? Value { get; set; } +} diff --git a/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs new file mode 100644 index 0000000000..565d66c3c8 --- /dev/null +++ b/framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel_Tests.cs @@ -0,0 +1,125 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Shouldly; +using Volo.Abp.Localization; +using Xunit; + +namespace Volo.Abp.Http.Modeling; + +public class PropertyApiDescriptionModel_Tests +{ + [Fact] + public void Create_Should_Write_Range_Bounds_With_The_Invariant_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var model = CreateModel(nameof(TestClass.DecimalRangeValue)); + + model.Minimum.ShouldBe("1.5"); + model.Maximum.ShouldBe("9.5"); + } + } + + [Fact] + public void Create_Should_Write_Typed_Range_Bounds_With_The_Invariant_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var model = CreateModel(nameof(TestClass.TypedDecimalRangeValue)); + + model.Minimum.ShouldBe("1.5"); + model.Maximum.ShouldBe("9.5"); + } + } + + [Fact] + public void Create_Should_Convert_A_Typed_Range_Bound_With_Its_Operand_Type() + { + // A magnitude outside the decimal range is still a valid double bound. + var model = CreateModel(nameof(TestClass.ExponentRangeValue)); + + model.Minimum.ShouldBe("1E-30"); + model.Maximum.ShouldBe("1E+30"); + } + + [Fact] + public void Create_Should_Convert_A_Typed_Double_Range_Bound_Written_In_Another_Culture() + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo("de-DE"))) + { + var model = CreateModel(nameof(TestClass.CultureExponentRangeValue)); + + model.Minimum.ShouldBe("1.5E+30"); + model.Maximum.ShouldBe("9.5E+30"); + } + } + + [Fact] + public void Create_Should_Keep_A_Typed_Range_Bound_That_Is_Not_A_Number() + { + var model = CreateModel(nameof(TestClass.DateRangeValue)); + + model.Minimum.ShouldBe("2020-01-01"); + model.Maximum.ShouldBe("2030-01-01"); + } + + [Fact] + public void Create_Should_Read_The_Exclusive_Bounds_Of_The_Range_Attribute() + { + var model = CreateModel(nameof(TestClass.ExclusiveRangeValue)); + + model.Minimum.ShouldBe("1"); + model.Maximum.ShouldBe("100"); + model.MinimumIsExclusive.ShouldBe(true); + model.MaximumIsExclusive.ShouldBe(true); + } + + [Fact] + public void Create_Should_Report_An_Inclusive_Range_Attribute_As_Inclusive() + { + var model = CreateModel(nameof(TestClass.DecimalRangeValue)); + + model.MinimumIsExclusive.ShouldBe(false); + model.MaximumIsExclusive.ShouldBe(false); + } + + [Fact] + public void Create_Should_Leave_The_Exclusive_Bounds_Null_Without_A_Range_Attribute() + { + var model = CreateModel(nameof(TestClass.UnconstrainedValue)); + + model.Minimum.ShouldBeNull(); + model.Maximum.ShouldBeNull(); + model.MinimumIsExclusive.ShouldBeNull(); + model.MaximumIsExclusive.ShouldBeNull(); + } + + private static PropertyApiDescriptionModel CreateModel(string propertyName) + { + return PropertyApiDescriptionModel.Create(typeof(TestClass).GetProperty(propertyName)!); + } + + public class TestClass + { + [Range(1.5, 9.5)] + public double DecimalRangeValue { get; set; } + + [Range(typeof(decimal), "1,5", "9,5")] + public decimal TypedDecimalRangeValue { get; set; } + + [Range(typeof(double), "1e-30", "1e30", ParseLimitsInInvariantCulture = true)] + public double ExponentRangeValue { get; set; } + + [Range(typeof(double), "1,5E+30", "9,5E+30")] + public double CultureExponentRangeValue { get; set; } + + [Range(typeof(DateTime), "2020-01-01", "2030-01-01")] + public DateTime DateRangeValue { get; set; } + + [Range(1, 100, MinimumIsExclusive = true, MaximumIsExclusive = true)] + public int ExclusiveRangeValue { get; set; } + + public string? UnconstrainedValue { get; set; } + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyQueryProjection_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyQueryProjection_Tests.cs new file mode 100644 index 0000000000..6715fdfa74 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyQueryProjection_Tests.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyQueryProjection_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Project_A_Queryable() + { + var queryProjector = ServiceProvider.GetRequiredService>(); + + var entities = new List + { + new MyEntity { Id = Guid.NewGuid(), Number = 42 } + }.AsQueryable(); + + var dtos = queryProjector.ProjectTo(entities).ToList(); + + dtos.Count.ShouldBe(1); + dtos[0].Id.ShouldBe(entities.First().Id); + dtos[0].Number.ShouldBe(42); + } + + + [Fact] + public void Should_Register_A_Projector_Only_Once() + { + //MyEntityQueryProjector also matches the class name convention of ExposeServicesAttribute + ServiceProvider.GetServices>().ShouldHaveSingleItem(); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs new file mode 100644 index 0000000000..980e40a94b --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityQueryProjector.cs @@ -0,0 +1,11 @@ +using System.Linq; +using Riok.Mapperly.Abstractions; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly.SampleClasses; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class MyEntityQueryProjector : IQueryProjector +{ + public partial IQueryable ProjectTo(IQueryable source); +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionAppService.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionAppService.cs new file mode 100644 index 0000000000..d625b518e8 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionAppService.cs @@ -0,0 +1,15 @@ +using System; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.MongoDB.Applications; + +public class PersonProjectionAppService : ReadOnlyAppService +{ + public PersonProjectionAppService(IReadOnlyRepository repository) + : base(repository) + { + + } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionDto.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionDto.cs new file mode 100644 index 0000000000..2a8e716a8c --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjectionDto.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Volo.Abp.MongoDB.Applications; + +public class PersonProjectionDto : EntityDto +{ + public string Name { get; set; } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjector.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjector.cs new file mode 100644 index 0000000000..eac0c6fd3a --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/PersonProjector.cs @@ -0,0 +1,17 @@ +using System.Linq; +using Volo.Abp.ObjectMapping; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.MongoDB.Applications; + +public class PersonProjector : IQueryProjector +{ + public IQueryable ProjectTo(IQueryable source) + { + return source.Select(person => new PersonProjectionDto + { + Id = person.Id, + Name = person.Name + }); + } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/QueryProjection_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/QueryProjection_Tests.cs new file mode 100644 index 0000000000..a7bf1367a6 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Applications/QueryProjection_Tests.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Entities; +using Volo.Abp.TestApp; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.MongoDB.Applications; + +[Collection(MongoTestCollection.Name)] +public class QueryProjection_Tests : MongoDbTestBase +{ + private readonly PersonProjectionAppService _personProjectionAppService; + + public QueryProjection_Tests() + { + _personProjectionAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_A_Projected_Entity() + { + var dto = await _personProjectionAppService.GetAsync(TestDataBuilder.UserDouglasId); + + dto.Id.ShouldBe(TestDataBuilder.UserDouglasId); + dto.Name.ShouldBe("Douglas"); + } + + [Fact] + public async Task Should_Throw_EntityNotFoundException_For_A_Missing_Entity() + { + await Should.ThrowAsync>( + async () => await _personProjectionAppService.GetAsync(Guid.NewGuid())); + } + + [Fact] + public async Task Should_Get_A_Projected_List() + { + var result = await _personProjectionAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(x => x.Name == "Douglas"); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ko.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ko.json new file mode 100644 index 0000000000..f74050f8d5 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ko.json @@ -0,0 +1,75 @@ +{ + "culture": "ko", + "texts": { + "Menu:Account": "계정", + "UserName": "사용자 이름", + "EmailAddress": "이메일 주소", + "UserNameOrEmailAddress": "사용자 이름 또는 이메일 주소", + "Password": "비밀번호", + "RememberMe": "로그인 상태 유지", + "UseAnotherServiceToLogin": "다른 서비스를 사용하여 로그인", + "UserLockedOutMessage": "잘못된 로그인 시도로 인해 사용자 계정이 잠겼습니다. 잠시 후 다시 시도해 주세요.", + "InvalidUserNameOrPassword": "사용자 이름 또는 비밀번호가 올바르지 않습니다!", + "LoginIsNotAllowed": "로그인할 수 없습니다! 계정이 비활성 상태이거나 이메일 또는 전화번호 확인이 필요합니다.", + "SelfRegistrationDisabledMessage": "이 애플리케이션에서는 자체 회원가입이 비활성화되어 있습니다. 새 사용자를 등록하려면 애플리케이션 관리자에게 문의해 주세요.", + "LocalLoginDisabledMessage": "이 애플리케이션에서는 로컬 로그인이 비활성화되어 있습니다.", + "Login": "로그인", + "Cancel": "취소", + "Register": "회원가입", + "AreYouANewUser": "신규 사용자이신가요?", + "AlreadyRegistered": "이미 가입하셨나요?", + "InvalidLoginRequest": "잘못된 로그인 요청", + "ThereAreNoLoginSchemesConfiguredForThisClient": "이 클라이언트에 구성된 로그인 스키마가 없습니다.", + "LogInUsingYourProviderAccount": "{0} 계정으로 로그인", + "DisplayName:CurrentPassword": "현재 비밀번호", + "DisplayName:NewPassword": "새 비밀번호", + "DisplayName:NewPasswordConfirm": "새 비밀번호 확인", + "PasswordChangedMessage": "비밀번호가 변경되었습니다.", + "DisplayName:UserName": "사용자 이름", + "DisplayName:Email": "이메일", + "DisplayName:Name": "이름", + "DisplayName:Surname": "성", + "DisplayName:Password": "비밀번호", + "DisplayName:EmailAddress": "이메일 주소", + "DisplayName:PhoneNumber": "전화번호", + "PersonalSettings": "개인 설정", + "PersonalSettingsSaved": "개인 설정이 저장되었습니다.", + "PersonalSettingsChangedConfirmationModalTitle": "개인 정보 변경됨", + "PersonalSettingsChangedConfirmationModalDescription": "변경 사항은 다시 로그인한 후 반영됩니다. 지금 로그아웃하시겠습니까?", + "PasswordChanged": "비밀번호 변경됨", + "NewPasswordConfirmFailed": "새 비밀번호를 확인해 주세요.", + "NewPasswordSameAsOld": "새 비밀번호는 기존 비밀번호와 달라야 합니다.", + "Manage": "관리", + "MyAccount": "내 계정", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "자체 회원가입 활성화 여부", + "Description:Abp.Account.IsSelfRegistrationEnabled": "사용자가 직접 계정을 등록할 수 있는지 여부입니다.", + "DisplayName:Abp.Account.EnableLocalLogin": "로컬 계정으로 인증", + "Description:Abp.Account.EnableLocalLogin": "서버에서 사용자의 로컬 계정 인증을 허용할지 여부를 나타냅니다.", + "LoggedOutTitle": "로그아웃됨", + "LoggedOutText": "로그아웃되었습니다. 잠시 후 리디렉션됩니다.", + "ReturnToText": "애플리케이션으로 돌아가려면 여기를 클릭하세요.", + "OrLoginWith": "또는 다음 계정으로 로그인:", + "ForgotPassword": "비밀번호를 잊으셨나요?", + "SendPasswordResetLink_Information": "비밀번호 재설정 링크가 이메일로 전송됩니다. 몇 분 이내에 이메일을 받지 못한 경우 다시 시도해 주세요.", + "PasswordResetMailSentMessage": "계정 복구 이메일을 이메일 주소로 전송했습니다. 15분 이내에 받은편지함에서 이메일을 찾을 수 없다면 스팸 메일함을 확인해 주세요. 스팸 메일함에서 찾은 경우 스팸이 아닌 것으로 표시해 주세요. ", + "ResetPassword": "비밀번호 재설정", + "ConfirmPassword": "비밀번호 확인", + "ResetPassword_Information": "새 비밀번호를 입력해 주세요.", + "YourPasswordIsSuccessfullyReset": "비밀번호가 재설정되었습니다.", + "GoToTheApplication": "애플리케이션으로 이동", + "BackToLogin": "로그인으로 돌아가기", + "ProfileTab:Password": "비밀번호 변경", + "ProfileTab:PersonalInfo": "개인 정보", + "ReturnToApplication": "애플리케이션으로 돌아가기", + "Volo.Account:InvalidEmailAddress": "지정한 이메일 주소를 찾을 수 없습니다: {0}", + "PasswordReset": "비밀번호 재설정", + "PasswordResetInfoInEmail": "계정 복구 요청이 접수되었습니다! 본인이 요청한 경우 다음 링크를 클릭하여 비밀번호를 재설정하세요.", + "ResetMyPassword": "내 비밀번호 재설정", + "AccessDenied": "액세스가 거부되었습니다!", + "AccessDeniedMessage": "이 리소스에 액세스할 권한이 없습니다.", + "OrRegisterWith": "또는 다음 계정으로 회원가입", + "RegisterUsingYourProviderAccount": "{0} 계정으로 회원가입", + "RequireMigrateSeedTitle": "관리자 사용자를 찾을 수 없습니다.", + "RequireMigrateSeedMessage": "데이터베이스 시드가 실행되었는지 확인해 주세요. 해결 방법은 문서를 참조하세요." + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt.json new file mode 100644 index 0000000000..df64691b13 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt.json @@ -0,0 +1,75 @@ +{ + "culture": "pt", + "texts": { + "Menu:Account": "Conta", + "UserName": "Nome de utilizador", + "EmailAddress": "Endereço de email", + "UserNameOrEmailAddress": "Nome de utilizador ou endereço de email", + "Password": "Palavra-passe", + "RememberMe": "Manter sessão iniciada", + "UseAnotherServiceToLogin": "Utilizar outro serviço para iniciar sessão", + "UserLockedOutMessage": "A sua conta foi bloqueada pelo administrador ou devido a tentativas de início de sessão inválidas. Tente novamente mais tarde. Contacte o administrador de sistema se considerar que se trata de um erro.", + "InvalidUserNameOrPassword": "Nome de utilizador ou palavra-passe inválidos!", + "LoginIsNotAllowed": "Não tem permissão para iniciar sessão! A sua conta está inativa ou precisa de confirmar o seu email/número de telefone.", + "SelfRegistrationDisabledMessage": "O registo autónomo está desativado nesta aplicação. Contacte o administrador da aplicação para registar um novo utilizador.", + "LocalLoginDisabledMessage": "O início de sessão local está desativado nesta aplicação.", + "Login": "Iniciar sessão", + "Cancel": "Cancelar", + "Register": "Registar", + "AreYouANewUser": "É um novo utilizador?", + "AlreadyRegistered": "Já está registado?", + "InvalidLoginRequest": "Pedido de início de sessão inválido", + "ThereAreNoLoginSchemesConfiguredForThisClient": "Não existem esquemas de início de sessão configurados para este cliente.", + "LogInUsingYourProviderAccount": "Inicie sessão com a sua conta {0}", + "DisplayName:CurrentPassword": "Palavra-passe atual", + "DisplayName:NewPassword": "Nova palavra-passe", + "DisplayName:NewPasswordConfirm": "Confirmar nova palavra-passe", + "PasswordChangedMessage": "A sua palavra-passe foi alterada com sucesso.", + "DisplayName:UserName": "Nome de utilizador", + "DisplayName:Email": "Email", + "DisplayName:Name": "Nome", + "DisplayName:Surname": "Apelido", + "DisplayName:Password": "Palavra-passe", + "DisplayName:EmailAddress": "Endereço de email", + "DisplayName:PhoneNumber": "Número de telefone", + "PersonalSettings": "Definições pessoais", + "PersonalSettingsSaved": "Definições pessoais guardadas", + "PersonalSettingsChangedConfirmationModalTitle": "Informação pessoal alterada", + "PersonalSettingsChangedConfirmationModalDescription": "Para aplicar estas alterações, tem de iniciar sessão novamente. Pretende terminar sessão?", + "PasswordChanged": "Palavra-passe alterada", + "NewPasswordConfirmFailed": "Confirme a nova palavra-passe.", + "NewPasswordSameAsOld": "A nova palavra-passe tem de ser diferente da anterior.", + "Manage": "Gerir", + "MyAccount": "A minha conta", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Registo de novos utilizadores ativado", + "Description:Abp.Account.IsSelfRegistrationEnabled": "Define se um utilizador pode registar a sua própria conta.", + "DisplayName:Abp.Account.EnableLocalLogin": "Autenticação com uma conta local", + "Description:Abp.Account.EnableLocalLogin": "Indica se o servidor permite que os utilizadores se autentiquem com uma conta local.", + "LoggedOutTitle": "Sessão terminada", + "LoggedOutText": "A sua sessão foi terminada e será redirecionado em breve.", + "ReturnToText": "Clique aqui para voltar à aplicação", + "OrLoginWith": "Ou inicie sessão com:", + "ForgotPassword": "Esqueceu-se da palavra-passe?", + "SendPasswordResetLink_Information": "Será enviada para o seu email uma ligação para repor a palavra-passe. Se não receber o email dentro de alguns minutos, tente novamente.", + "PasswordResetMailSentMessage": "Foi enviado um email de recuperação de conta. Se não o receber em 15 minutos, verifique a pasta de correio não solicitado e marque-o como 'Não é spam'.", + "ResetPassword": "Repor palavra-passe", + "ConfirmPassword": "Confirme (repita) a palavra-passe", + "ResetPassword_Information": "Introduza a sua nova palavra-passe.", + "YourPasswordIsSuccessfullyReset": "A sua palavra-passe foi reposta com sucesso.", + "GoToTheApplication": "Ir para a aplicação", + "BackToLogin": "Voltar ao início de sessão", + "ProfileTab:Password": "Alterar palavra-passe", + "ProfileTab:PersonalInfo": "Informação pessoal", + "ReturnToApplication": "Voltar à aplicação", + "Volo.Account:InvalidEmailAddress": "Não foi possível encontrar o email indicado: {0}", + "PasswordReset": "Reposição de palavra-passe", + "PasswordResetInfoInEmail": "Recebemos um pedido de recuperação de conta! Se foi você que efetuou este pedido, clique na ligação seguinte para repor a sua palavra-passe.", + "ResetMyPassword": "Repor a minha palavra-passe", + "AccessDenied": "Acesso negado!", + "AccessDeniedMessage": "Não tem acesso a este recurso.", + "OrRegisterWith": "Ou registe-se com", + "RegisterUsingYourProviderAccount": "Registe-se com a sua conta {0}", + "RequireMigrateSeedTitle": "Não foi possível encontrar o utilizador administrador", + "RequireMigrateSeedMessage": "Certifique-se de que o seed da base de dados foi executado. Consulte a documentação para possíveis soluções." + } +} diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml index e012bdd3f4..75c9c472e9 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml @@ -1,7 +1,9 @@ -@using Volo.Abp.Account.Localization +@using Localization.Resources.AbpUi +@using Volo.Abp.Account.Localization @using Volo.Abp.Users @using Microsoft.AspNetCore.Mvc.Localization @inject IHtmlLocalizer L +@inject IHtmlLocalizer UiL @inject ICurrentUser CurrentUser @model Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password.AccountProfilePasswordManagementGroupViewComponent.ChangePasswordInfoModel @@ -13,21 +15,21 @@
- +

}
- +

- +
diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js index ec23761729..ca89718adb 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js @@ -6,12 +6,9 @@ return; } - if (passwordInput.attr("type") === "password") { - passwordInput.attr("type", "text"); - } - else { - passwordInput.attr("type", "password"); - } + let isRevealing = passwordInput.attr("type") === "password"; + passwordInput.attr("type", isRevealing ? "text" : "password"); + button.attr("aria-pressed", isRevealing); let icon = button.find("i"); if (icon) { diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml index 1c883936b4..4fd5e6d5c3 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml @@ -1,4 +1,5 @@ @page +@using Localization.Resources.AbpUi @using Microsoft.AspNetCore.Mvc.Localization @using Volo.Abp.Account.Localization @using Volo.Abp.Account.Settings @@ -8,6 +9,7 @@ @using Volo.Abp.Settings @model Volo.Abp.Account.Web.Pages.Account.LoginModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer UiL @inject IThemeManager ThemeManager @inject Volo.Abp.Settings.ISettingProvider SettingProvider @@ -53,7 +55,7 @@
- +
diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.js b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.js index 56934f4fef..381f414305 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.js +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.js @@ -6,12 +6,9 @@ $(function () { return; } - if (passwordInput.attr("type") === "password") { - passwordInput.attr("type", "text"); - } - else { - passwordInput.attr("type", "password"); - } + let isRevealing = passwordInput.attr("type") === "password"; + passwordInput.attr("type", isRevealing ? "text" : "password"); + button.attr("aria-pressed", isRevealing); let icon = button.find("i"); if (icon) { diff --git a/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ko.json b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ko.json new file mode 100644 index 0000000000..a08ef4a57d --- /dev/null +++ b/modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/ko.json @@ -0,0 +1,118 @@ +{ + "culture": "ko", + "texts": { + "Permission:AuditLogging": "감사 로깅", + "Permission:AuditLogs": "감사 로그", + "Menu:AuditLogging": "감사 로그", + "AuditLogs": "감사 로그", + "HttpStatus": "HTTP 상태", + "HttpMethod": "HTTP 메서드", + "HttpMethodFilter": "HTTP 메서드 필터", + "HttpRequest": "HTTP 요청", + "User": "사용자", + "UserNameFilter": "사용자 필터", + "HasException": "예외 발생 여부", + "IpAddress": "IP 주소", + "Time": "시간", + "Date": "날짜", + "Duration": "소요 시간", + "Detail": "세부 정보", + "Overall": "전체", + "Actions": "작업", + "ClientIpAddress": "클라이언트 IP 주소", + "ClientName": "클라이언트 이름", + "BrowserInfo": "브라우저 정보", + "Url": "URL", + "UserName": "사용자 이름", + "TenantImpersonator": "테넌트 가장 사용자", + "UserImpersonator": "사용자 가장 사용자", + "UrlFilter": "URL 필터", + "Exceptions": "예외", + "Comments": "설명", + "HttpStatusCode": "HTTP 상태 코드", + "HttpStatusCodeFilter": "HTTP 상태 코드 필터", + "ServiceName": "서비스", + "MethodName": "메서드", + "CorrelationId": "상관관계 ID", + "ApplicationName": "애플리케이션 이름", + "ExecutionDuration": "실행 시간", + "ExtraProperties": "추가 속성", + "MaxDuration": "최대 소요 시간", + "MinDuration": "최소 소요 시간", + "MinMaxDuration": "소요 시간(최소~최대)", + "{0}Milliseconds": "{0}밀리초", + "ExecutionTime": "실행 시간", + "Parameters": "매개변수", + "EntityTypeFullName": "엔터티 형식 전체 이름", + "Entity": "엔터티", + "ChangeType": "변경 형식", + "ChangeTime": "변경 시간", + "NewValue": "새 값", + "OriginalValue": "이전 값", + "PropertyName": "속성 이름", + "PropertyTypeFullName": "속성 형식 전체 이름", + "Yes": "예", + "No": "아니요", + "Changes": "변경 사항", + "AverageExecutionDurationInLogsPerDay": "평균 실행 시간", + "AverageExecutionDurationInMilliseconds": "평균 실행 시간(밀리초)", + "ErrorRateInLogs": "로그 오류율", + "Success": "성공", + "Fault": "실패", + "NoChanges": "변경 사항 없음", + "EntityChanges": "엔터티 변경 사항", + "EntityId": "엔터티 ID", + "EntityChangeStartTime": "최소 변경 날짜", + "EntityChangeEndTime": "최대 변경 날짜", + "EntityHistory": "엔터티 기록", + "DaysAgoTitle": "{0} {1}.", + "DaysAgoWithUserTitle": "{0} {1}, {2} 사용자가 수행했습니다.", + "MinutesAgo": "{0}분 전", + "HoursAgo": "{0}시간 전", + "DaysAgo": "{0}일 전", + "Created": "생성함", + "Updated": "수정함", + "Deleted": "삭제함", + "ChangeHistory": "변경 기록", + "FullChangeHistory": "전체 변경 기록", + "ChangeDetails": "변경 세부 정보", + "DurationMs": "소요 시간(ms)", + "StartDate": "시작 날짜", + "EndDate": "종료 날짜", + "Feature:AuditLoggingGroup": "감사 로깅", + "Feature:AuditLoggingEnable": "감사 로그 페이지 사용", + "Feature:AuditLoggingEnableDescription": "애플리케이션에서 감사 로그 페이지를 사용합니다.", + "Feature:AuditLoggingSettingManagementEnable": "감사 로그 설정 관리 사용", + "Feature:AuditLoggingSettingManagementEnableDescription": "애플리케이션에서 감사 로그 설정 관리를 구성할 수 있습니다.", + "InvalidAuditLogDeletionSettings": "감사 로그 삭제 설정이 올바르지 않습니다. 삭제를 사용하는 경우 기간은 0일보다 커야 합니다.", + "AuditLogSettingsGeneral": "일반", + "AuditLogSettingsGlobal": "전역", + "DisplayName:IsPeriodicDeleterEnabled": "시스템 전체에서 정리 서비스 사용", + "Description:IsPeriodicDeleterEnabled": "이 옵션을 사용하지 않으면 주기적 삭제 서비스가 작동하지 않으며 감사 로그가 자동으로 삭제되지 않습니다.", + "DisplayName:GlobalIsExpiredDeleterEnabled": "모든 테넌트와 호스트에 정리 서비스 사용", + "Description:GlobalIsExpiredDeleterEnabled": "이 옵션을 사용하면 별도 설정이 없는 모든 테넌트와 호스트의 만료된 항목이 자동으로 삭제됩니다.", + "DisplayName:IsExpiredDeleterEnabled": "정리 서비스 사용", + "Description:IsExpiredDeleterEnabled": "이 옵션을 사용하면 만료된 항목이 자동으로 삭제됩니다.", + "DisplayName:ExpiredDeleterPeriod": "만료 항목 삭제 기간", + "Description:ExpiredDeleterPeriod": "만료된 항목이 자동으로 삭제될 때까지의 일수를 설정합니다.", + "ExpiredDeleterPeriodUnit": "일", + "AuditLogsBeforeXWillBeDeleted": "{0} 이전의 감사 로그가 삭제됩니다.", + "TenantId": "테넌트 ID", + "Permission:Export": "감사 로그 내보내기", + "ExportToExcel": "Excel로 내보내기", + "Exporting": "내보내는 중", + "ExportCompleted": "내보내기가 완료되었습니다.", + "ExportFailed": "내보내기에 실패했습니다.", + "ThereWereNoRecordsToExport": "내보낼 레코드가 없습니다.", + "ExportJobQueued": "{0}개 레코드에 대한 내보내기 작업이 대기열에 추가되었습니다. 내보내기가 완료되면 이메일을 보내드립니다.", + "ExportReady": "{0}개 레코드 내보내기가 완료되어 다운로드할 수 있습니다.", + "FileName": "파일 이름", + "TotalRecords": "전체 레코드 수", + "ExcelFileAttachedMessage": "Excel 파일이 이 이메일에 첨부되어 있습니다.", + "EntityChangeExportCompletedSubject": "엔터티 변경 사항 내보내기 완료", + "AuditLogExportCompletedSubject": "감사 로그 내보내기 완료", + "DownloadLinkExplanation": "{0}(UTC)까지 아래 링크를 사용하여 파일을 다운로드할 수 있습니다.", + "DownloadNow": "지금 다운로드", + "TryAgainMessage": "다시 시도해 주세요. 문제가 계속되면 지원팀에 문의해 주세요." + } +} diff --git a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain.Shared/Volo/Abp/BlobStoring/Database/Localization/ko.json b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain.Shared/Volo/Abp/BlobStoring/Database/Localization/ko.json new file mode 100644 index 0000000000..17fcfd79c4 --- /dev/null +++ b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain.Shared/Volo/Abp/BlobStoring/Database/Localization/ko.json @@ -0,0 +1,6 @@ +{ + "culture": "ko", + "texts": { + "MyAccount": "내 계정" + } +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/ko.json b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/ko.json new file mode 100644 index 0000000000..0aa35fe93e --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo/Blogging/Localization/Resources/ko.json @@ -0,0 +1,93 @@ +{ + "culture": "ko", + "texts": { + "Menu:Blogs": "블로그", + "Menu:BlogManagement": "블로그 관리", + "Permission:Management": "관리", + "Permission:Edit": "편집", + "Permission:Create": "생성", + "Permission:Delete": "삭제", + "Permission:Blogging": "블로그", + "Permission:Blogs": "블로그", + "Permission:Posts": "게시물", + "Permission:Tags": "태그", + "Permission:Comments": "댓글", + "Permission:ClearCache": "캐시 지우기", + "Title": "제목", + "Delete": "삭제", + "Reply": "답글", + "ReplyTo": "{0}에 답글 달기", + "ContinueReading": "계속 읽기", + "DaysAgo": "{0}일 전", + "DayAgo": "{0}일 전", + "YearsAgo": "{0}년 전", + "YearAgo": "{0}년 전", + "MonthsAgo": "{0}개월 전", + "MonthAgo": "{0}개월 전", + "WeeksAgo": "{0}주 전", + "WeekAgo": "{0}주 전", + "MinutesAgo": "{0}분 전", + "MinuteAgo": "{0}분 전", + "SecondsAgo": "{0}초 전", + "SecondAgo": "{0}초 전", + "HoursAgo": "{0}시간 전", + "HourAgo": "{0}시간 전", + "Now": "지금", + "Content": "내용", + "SeeAll": "모두 보기", + "PopularTags": "인기 태그", + "WiewsWithCount": "조회수 {0}회", + "LastPosts": "최신 게시물", + "LeaveComment": "댓글 남기기", + "TagsInThisArticle": "이 글의 태그", + "Posts": "게시물", + "Edit": "편집", + "BLOG": "블로그", + "CommentDeletionWarningMessage": "댓글이 삭제됩니다.", + "PostDeletionWarningMessage": "게시물이 삭제됩니다.", + "BlogDeletionWarningMessage": "블로그가 삭제됩니다.", + "AreYouSure": "계속하시겠습니까?", + "CommentWithCount": "댓글 {0}개", + "Comment": "댓글", + "ShareOnTwitter": "트위터에 공유", + "CoverImage": "커버 이미지", + "CreateANewPost": "새 게시물 만들기", + "CreateANewBlog": "새 블로그 만들기", + "WhatIsNew": "새로운 소식", + "Name": "이름", + "ShortName": "짧은 이름", + "CreationTime": "생성 시간", + "Description": "설명", + "Blogs": "블로그", + "Tags": "태그", + "ShareOn": "공유", + "TitleLengthWarning": "SEO에 적합하도록 제목을 60자 이내로 작성해야 합니다.", + "ClearCache": "캐시 지우기", + "ClearCacheConfirmationMessage": "캐시를 지우시겠습니까?", + "MarkdownSupported": "Markdown을 지원합니다.", + "FileUploadInfo": "이미지를 끌어다 놓거나 복사한 이미지를 붙여넣을 수 있습니다.", + "PostDescriptionHint": "* 게시물 링크 미리 보기에 표시되며 HTML을 지원합니다.", + "ReadMore": "계속 읽기", + "MemberNotPublishedPostYet": "아직 게시물이 없습니다!", + "UpdateUserWebSiteInfo": "예: https://johndoe.com", + "UpdateUserTwitterInfo": "예: johndoe", + "UpdateUserGithubInfo": "예: johndoe", + "UpdateUserLinkedinInfo": "예: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "예: Volosoft", + "UpdateUserJobTitleInfo": "예: 소프트웨어 개발자", + "WebSite": "웹사이트", + "UserName": "사용자 이름", + "FullURL": "전체 URL", + "JobTitle": "직책", + "PersonalWebsite": "개인 웹사이트", + "EditProfile": "프로필 편집", + "MoreFromBlog": "블로그의 다른 글", + "MoreFromUser": "{0}님의 다른 글", + "BlogPosts": "게시물", + "Views": "조회수", + "Biography": "소개", + "Social": "소셜 미디어", + "NewBlogPost": "새 블로그 게시물", + "BlogMemberMetaDescription": ".NET 개발, 크로스 플랫폼, ASP.NET 애플리케이션 템플릿, ABP 관련 소식 등을 다루는 ABP 블로그입니다." + } +} diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml index a0ca2d81e3..4278e807ad 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml @@ -1,7 +1,6 @@ @page @using Microsoft.AspNetCore.Authorization @using Volo.Blogging.Areas.Blog.Helpers.TagHelpers -@using Volo.Abp.AspNetCore.Mvc.UI.Packages.OwlCarousel @using Volo.Blogging @inject IAuthorizationService Authorization @model IndexModel @@ -19,13 +18,11 @@ @section scripts { - } @section styles { - } diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js index ca0b21d203..37cd1643bb 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Shared/Scripts/blog.js @@ -1,11 +1,4 @@ (function () { - function handleArrows() { - var herosWidth = $('.hero-articles').width(); - var arrowsPosition = herosWidth / 2 - 90; - $('.owl-next').css('right', arrowsPosition); - $('.owl-prev').css('left', arrowsPosition); - } - function handleImages() { //if ($(window).width() > 767) { // $(".box-articles .img-container").each(function () { @@ -18,36 +11,8 @@ //} } - function handleOwlCarousel() { - $('.hero-section .owl-carousel').owlCarousel({ - loop: true, - margin: 0, - nav: false, - dots: false, - //center: true, - autoplay: true, - autoHeight: true, - autoplaySpeed: 1000, - items: 1, - }); - $('.card-article-container .owl-carousel').owlCarousel({ - loop: true, - margin: 0, - nav: false, - dots: false, - autoplay: true, - autoplaySpeed: 1000, - responsive: { - 0: { - items: 1, - }, - }, - }); - } - $(function () { handleImages(); - handleOwlCarousel(); $('.nav-link').on('click', function () { $(this).parent().parent(); @@ -55,13 +20,8 @@ $(window).resize(function () { setTimeout(function () { - handleArrows(); handleImages(); }, 500); }); - - setTimeout(function () { - handleArrows(); - }, 500); }); })(); diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ko.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ko.json new file mode 100644 index 0000000000..f844f1cf7e --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/ko.json @@ -0,0 +1,290 @@ +{ + "culture": "ko", + "texts": { + "AddSubMenuItem": "하위 메뉴 항목 추가", + "AreYouSure": "계속하시겠습니까?", + "AverageRating": "평균 평점", + "BlogDeletionConfirmationMessage": "블로그 '{0}'을(를) 삭제합니다. 계속하시겠습니까?", + "BlogFeatureNotAvailable": "현재 이 기능을 사용할 수 없습니다. 사용하려면 'GlobalFeatureManager'에서 활성화하세요.", + "BlogId": "블로그", + "BlogPostDeletionConfirmationMessage": "블로그 게시물 '{0}'을(를) 삭제합니다. 계속하시겠습니까?", + "BlogPosts": "블로그 게시물", + "Blogs": "블로그", + "ChoosePreference": "환경 설정 선택...", + "Cms": "CMS", + "CmsKit.Comments": "댓글", + "CmsKit.Ratings": "평점", + "CmsKit.Reactions": "반응", + "CmsKit.Tags": "태그", + "CmsKit.MarkedItems": "표시된 항목", + "CmsKit:0002": "콘텐츠가 이미 존재합니다!", + "CmsKit:0003": "엔터티 {0}은(는) 태그를 지정할 수 없습니다.", + "CmsKit:Blog:0001": "지정한 슬러그({Slug})가 이미 존재합니다!", + "CmsKit:BlogPost:0001": "지정한 슬러그가 이미 존재합니다!", + "CmsKit:Comments:0001": "엔터티 {EntityType}에는 댓글을 작성할 수 없습니다.", + "CmsKit:Media:0001": "'{Name}'은(는) 유효한 미디어 이름이 아닙니다.", + "CmsKit:Media:0002": "이 엔터티에는 미디어를 추가할 수 없습니다.", + "CmsKit:Page:0001": "지정한 URL({Slug})이(가) 이미 존재합니다. 다른 URL을 사용하세요.", + "CmsKit:Rating:0001": "엔터티 {EntityType}에는 평점을 매길 수 없습니다.", + "CmsKit:Reaction:0001": "엔터티 {EntityType}에는 반응을 추가할 수 없습니다.", + "CmsKit:Tag:0002": "이 엔터티에는 태그를 지정할 수 없습니다!", + "CmsKit:MarkedItem:ToggleConfirmation": "표시된 항목 상태를 전환하시겠습니까?", + "ToggleFavorite": "즐겨찾기 추가/제거", + "FavoritesFilterMessage": "즐겨찾기를 필터링하려면 로그인하세요.", + "FilterOnFavorites": "즐겨찾기로 필터링", + "CommentAuthorizationExceptionMessage": "해당 댓글은 공개적으로 표시할 수 없습니다.", + "CmsKit:Modals:Login": "로그인", + "CmsKit:Modals:LoginModalDefaultMessage": "계속하려면 로그인하세요!", + "CmsKit:Modals:YouAreNotAuthenticated": "이 작업을 수행할 권한이 없습니다.", + "CommentDeletionConfirmationMessage": "이 댓글과 모든 답글을 삭제합니다!", + "CmsKit:MarkedItem:0001": "엔터티 {EntityType}은(는) 표시할 수 없습니다.", + "CmsKit:MarkedItem:0002": "엔터티 유형 '{EntityType}'에 대한 정의를 찾을 수 없습니다.", + "CmsKit:MarkedItem:0003": "엔터티 유형 '{EntityType}'에 대한 정의가 이미 존재합니다. 각 엔터티 유형에는 하나의 정의만 있어야 합니다.", + "CmsKit:MarkedItem:LoginMessage": "이 항목을 표시하려면 로그인하세요.", + "Comments": "댓글", + "Content": "콘텐츠", + "ContentDeletionConfirmationMessage": "이 콘텐츠를 삭제하시겠습니까?", + "Contents": "콘텐츠", + "CoverImage": "표지 이미지", + "CreateBlogPostPage": "새 블로그 게시물", + "CreationTime": "생성 시간", + "Delete": "삭제", + "Detail": "상세 정보", + "Details": "상세 정보", + "DisplayName": "표시 이름", + "DoYouPreferAdditionalEmails": "추가 이메일을 받으시겠습니까?", + "Edit": "편집", + "EndDate": "종료일", + "EntityId": "엔터티 ID", + "EntityType": "엔터티 유형", + "ExportCSV": "CSV 내보내기", + "Features": "기능", + "GenericDeletionConfirmationMessage": "'{0}'을(를) 삭제하시겠습니까?", + "IsActive": "활성", + "LastModification": "마지막 수정", + "LastModificationTime": "마지막 수정 시간", + "LoginToAddComment": "댓글을 작성하려면 로그인하세요.", + "LoginToRate": "평점을 매기려면 로그인하세요.", + "LoginToReact": "반응을 남기려면 로그인하세요.", + "LoginToReply": "답글을 작성하려면 로그인하세요.", + "MainMenu": "주 메뉴", + "MakeMainMenu": "주 메뉴로 설정", + "Menu:CMS": "CMS", + "Menus": "메뉴", + "MenuDeletionConfirmationMessage": "메뉴 '{0}'을(를) 삭제합니다. 계속하시겠습니까?", + "MenuItemDeletionConfirmationMessage": "이 메뉴 항목을 삭제하시겠습니까?", + "MenuItemMoveConfirmMessage": "'{0}'을(를) '{1}' 아래로 이동하시겠습니까?", + "MenuItems": "메뉴 항목", + "Message": "메시지", + "MessageDeletionConfirmationMessage": "이 댓글을 완전히 삭제합니다.", + "NewBlog": "새 블로그", + "NewBlogPost": "새 블로그 게시물", + "NewMenu": "새 메뉴", + "NewMenuItem": "새 루트 메뉴 항목", + "NewPage": "새 페이지", + "NewTag": "새 태그", + "NoMenuItems": "아직 메뉴 항목이 없습니다!", + "OK": "확인", + "PageDeletionConfirmationMessage": "이 페이지를 삭제하시겠습니까?", + "PageId": "페이지", + "Pages": "페이지", + "PageSlugInformation": "슬러그는 URL에 사용됩니다. URL은 '/{{slug}}' 형식입니다.", + "BlogSlugInformation": "슬러그는 URL에 사용됩니다. URL은 '/{0}/{{slug}}' 형식입니다.", + "Permission:BlogManagement": "블로그 관리", + "Permission:BlogManagement.Create": "생성", + "Permission:BlogManagement.Delete": "삭제", + "Permission:BlogManagement.Features": "기능", + "Permission:BlogManagement.Update": "수정", + "Permission:BlogPostManagement": "블로그 게시물 관리", + "Permission:BlogPostManagement.Create": "생성", + "Permission:BlogPostManagement.Delete": "삭제", + "Permission:BlogPostManagement.Update": "수정", + "Permission:BlogPostManagement.Publish": "게시", + "Permission:CmsKit": "CmsKit 관리", + "Permission:Comments": "댓글 관리", + "Permission:Comments.Delete": "삭제", + "Permission:Comments.Update": "수정", + "Permission:Comments.SettingManagement": "설정 관리", + "Permission:Contents": "콘텐츠 관리", + "Permission:Contents.Create": "콘텐츠 생성", + "Permission:Contents.Delete": "콘텐츠 삭제", + "Permission:Contents.Update": "콘텐츠 수정", + "Permission:MediaDescriptorManagement": "미디어 관리", + "Permission:MediaDescriptorManagement:Create": "생성", + "Permission:MediaDescriptorManagement:Delete": "삭제", + "Permission:MenuItemManagement": "메뉴 항목 관리", + "Permission:MenuItemManagement.Create": "생성", + "Permission:MenuItemManagement.Delete": "삭제", + "Permission:MenuItemManagement.Update": "수정", + "Permission:MenuManagement": "메뉴 관리", + "Permission:MenuManagement.Create": "생성", + "Permission:MenuManagement.Delete": "삭제", + "Permission:MenuManagement.Update": "수정", + "Permission:Menus": "메뉴 관리", + "Permission:Menus.Create": "생성", + "Permission:Menus.Delete": "삭제", + "Permission:Menus.Update": "수정", + "Permission:PageManagement": "페이지 관리", + "Permission:PageManagement:Create": "생성", + "Permission:PageManagement:Delete": "삭제", + "Permission:PageManagement:Update": "수정", + "Permission:PageManagement:SetAsHomePage": "홈 페이지로 설정", + "Permission:TagManagement": "태그 관리", + "Permission:TagManagement.Create": "생성", + "Permission:TagManagement.Delete": "삭제", + "Permission:TagManagement.Update": "수정", + "Permission:GlobalResources": "전역 리소스", + "Permission:CmsKitPublic": "CmsKit 공개 기능", + "Permission:Comments.DeleteAll": "모두 삭제", + "PickYourReaction": "반응 선택", + "Rating": "평점", + "RatingUndoMessage": "평점이 취소됩니다.", + "Reactions": "반응", + "Read": "읽기", + "RepliesToThisComment": "이 댓글의 답글", + "Reply": "답글", + "ReplyTo": "답글 대상", + "SamplePageMessage": "Pro 모듈의 샘플 페이지", + "SaveChanges": "변경 사항 저장", + "Script": "스크립트", + "SelectLayout": "레이아웃 선택", + "SelectAll": "모두 선택", + "Send": "보내기", + "SendMessage": "메시지 보내기", + "SelectedAuthor": "작성자", + "ShortDescription": "간단한 설명", + "Slug": "슬러그", + "Source": "소스", + "SourceUrl": "소스 URL", + "Star": "별점", + "StartDate": "시작일", + "Style": "스타일", + "Subject": "제목", + "SubjectPlaceholder": "제목을 입력하세요.", + "Submit": "제출", + "Subscribe": "구독", + "SavedSuccessfully": "저장되었습니다!", + "TagDeletionConfirmationMessage": "태그 '{0}'을(를) 삭제하시겠습니까?", + "Tags": "태그", + "Text": "텍스트", + "ThankYou": "감사합니다.", + "Title": "제목", + "TotalRatings": "총 평점 수", + "Undo": "실행 취소", + "Update": "수정", + "UpdatePreferenceSuccessMessage": "환경 설정이 저장되었습니다.", + "UpdateYourEmailPreferences": "이메일 환경 설정 업데이트", + "UnMakeMainMenu": "주 메뉴 설정 해제", + "UploadFailedMessage": "업로드에 실패했습니다.", + "UserId": "사용자 ID", + "Username": "사용자 이름", + "YourComment": "내 댓글", + "YourEmailAddress": "이메일 주소", + "YourFullName": "전체 이름", + "YourMessage": "내 메시지", + "YourReply": "내 답글", + "MarkdownSupported": "Markdown을 지원합니다.", + "GlobalResources": "전역 리소스", + "CmsKit.BlogPost.Status.0": "초안", + "CmsKit.BlogPost.Status.1": "게시됨", + "CmsKit.BlogPost.Status.2": "검토 대기 중", + "BlogPostPublishConfirmationMessage": "블로그 게시물 \"{0}\"을(를) 게시하시겠습니까?", + "SuccessfullyPublished": "게시되었습니다!", + "Draft": "초안", + "Publish": "게시", + "BlogPostDraftConfirmationMessage": "블로그 게시물 \"{0}\"을(를) 초안으로 설정하시겠습니까?", + "BlogPostSendToReviewConfirmationMessage": "블로그 게시물 \"{0}\"을(를) 게시하기 위해 관리자 검토로 보내시겠습니까?", + "SaveAsDraft": "초안으로 저장", + "SendToReview": "검토 요청", + "SendToReviewToPublish": "게시 검토 요청", + "BlogPostSendToReviewSuccessMessage": "블로그 게시물 \"{0}\"이(가) 게시를 위한 관리자 검토로 전송되었습니다.", + "HasBlogPostWaitingForReviewMessage": "검토 대기 중인 블로그 게시물이 있습니다. 목록을 보려면 클릭하세요.", + "SelectAStatus": "상태 선택", + "Status": "상태", + "CmsKit.BlogPost.ScrollIndex": "블로그 게시물 빠른 탐색 모음", + "CmsKit.BlogPost.PreventXssFeature": "XSS 방지", + "Add": "추가", + "AddWidget": "위젯 추가", + "PleaseConfigureWidgets": "위젯을 구성하세요.", + "SelectAnAuthor": "작성자 선택", + "InThisDocument": "이 문서에서", + "GoToTop": "맨 위로 이동", + "SetAsHomePage": "홈 페이지 상태 변경", + "CompletedSettingAsHomePage": "홈 페이지로 설정됨", + "IsHomePage": "홈 페이지 여부", + "RemovedSettingAsHomePage": "홈 페이지 설정이 해제됨", + "Feature:CmsKitGroup": "CMS Kit", + "Feature:BlogEnable": "블로그 게시물", + "Feature:BlogEnableDescription": "애플리케이션에서 블로그와 게시물을 동적으로 생성할 수 있는 CMS Kit 블로그 게시물 시스템입니다.", + "Feature:CommentEnable": "댓글", + "Feature:CommentEnableDescription": "BlogPost와 같은 엔터티에 댓글을 작성할 수 있는 CMS Kit 댓글 시스템입니다.", + "Feature:GlobalResourceEnable": "전역 리소스", + "Feature:GlobalResourceEnableDescription": "전역 스타일과 스크립트를 관리할 수 있는 CMS Kit 전역 리소스 기능입니다.", + "Feature:MenuEnable": "메뉴", + "Feature:MenuEnableDescription": "애플리케이션 메뉴를 동적으로 추가하거나 제거할 수 있는 CMS Kit 동적 메뉴 시스템입니다.", + "Feature:PageEnable": "페이지", + "Feature:PageEnableDescription": "특정 URL을 사용하는 정적 페이지를 생성할 수 있는 CMS Kit 페이지 시스템입니다.", + "Feature:RatingEnable": "평점", + "Feature:RatingEnableDescription": "사용자가 BlogPost와 같은 엔터티에 평점을 매길 수 있는 CMS Kit 평점 시스템입니다.", + "Feature:ReactionEnable": "반응", + "Feature:ReactionEnableDescription": "사용자가 BlogPost, Comment 등의 엔터티에 반응을 남길 수 있는 CMS Kit 반응 시스템입니다.", + "Feature:TagEnable": "태그 지정", + "Feature:TagEnableDescription": "BlogPost와 같은 엔터티에 태그를 지정할 수 있는 CMS Kit 태그 시스템입니다.", + "Feature:MarkedItemEnable": "표시된 항목", + "Feature:MarkedItemEnableDescription": "사용자가 엔터티를 즐겨찾기로 표시할 수 있는 CMS Kit 표시 시스템입니다.", + "DeleteBlogPostMessage": "블로그를 삭제합니다. 계속하시겠습니까?", + "CaptchaCode": "CAPTCHA 코드", + "CommentTextRequired": "댓글은 필수입니다.", + "CaptchaCodeErrorMessage": "입력한 CAPTCHA 답변이 올바르지 않습니다. 다시 시도하세요.", + "CaptchaCodeMissingMessage": "CAPTCHA 코드가 누락되었습니다!", + "UnAllowedExternalUrlMessage": "허용되지 않은 외부 URL이 포함되어 있습니다. 외부 URL을 제거하고 다시 시도하세요.", + "URL": "URL", + "PopularTags": "인기 태그", + "RemoveCoverImageConfirmationMessage": "표지 이미지를 제거하시겠습니까?", + "RemoveCoverImage": "표지 이미지 제거", + "CssClass": "CSS 클래스", + "TagsHelpText": "태그는 쉼표로 구분해야 합니다(예: tag1, tag2, tag3).", + "ThisPartOfContentCouldntBeLoaded": "콘텐츠의 이 부분을 불러올 수 없습니다.", + "DuplicateCommentAttemptMessage": "중복 댓글 작성 시도가 감지되었습니다. 댓글이 이미 제출되었습니다.", + "NoBlogPostYet": "아직 블로그 게시물이 없습니다!", + "CmsKit:Comment": "댓글", + "CmsKitCommentOptions:RequireApprovement": "댓글 승인 필요", + "CmsKitCommentOptions:RequireApprovementDescription": "활성화하면 댓글을 게시하기 전에 승인이 필요합니다.", + "CommentFilter:ApproveState": "승인 상태", + "ApproveState": "승인 상태", + "CommentFilter:0": "전체", + "CommentFilter:1": "승인됨", + "CommentFilter:2": "승인 거부됨", + "CommentFilter:4": "대기 중", + "ApprovedSuccessfully": "승인되었습니다.", + "ApprovalRevokedSuccessfully": "승인이 취소되었습니다.", + "Approve": "승인", + "Disapproved": "승인 거부됨", + "CommentAlertMessage": "승인 대기 중인 댓글이 {0}개 있습니다.", + "Settings:Menu:CmsKit": "CMS", + "CommentsAwaitingApproval": "승인 대기 중인 댓글", + "JustNow": "방금", + "MinuteAgo": "1분 전", + "MinutesAgo": "{0}분 전", + "HourAgo": "1시간 전", + "HoursAgo": "{0}시간 전", + "YesterdayAt": "어제 {0}", + "DayAt": "{0} {1}", + "MonthDayAt": "{0} {1} {2}", + "FullDate": "{0} {1} {2}", + "Minute": "분", + "Hour": "시간", + "Day": "일", + "Week": "주", + "CommentSubmittedForApproval": "댓글이 승인 요청으로 제출되었습니다.", + "ChooseAnActionForBlog": "블로그 작업 선택", + "AssignBlogPostsToOtherBlog": "블로그 게시물을 다른 블로그에 할당", + "SelectAnBlogToAssign": "할당할 블로그 선택", + "DeleteAllBlogPostsOfThisBlog": "이 블로그의 모든 게시물 삭제", + "RequiredPermissionName": "필수 권한 이름", + "Enum:PageStatus:0": "초안", + "Enum:PageStatus:1": "게시", + "AllPosts": "모든 게시물", + "IsReadOnly": "읽기 전용" + } +} \ No newline at end of file diff --git a/modules/docs/app/VoloDocs.Web/Localization/Resources/VoloDocs/Web/ko.json b/modules/docs/app/VoloDocs.Web/Localization/Resources/VoloDocs/Web/ko.json new file mode 100644 index 0000000000..e4ddba2392 --- /dev/null +++ b/modules/docs/app/VoloDocs.Web/Localization/Resources/VoloDocs/Web/ko.json @@ -0,0 +1,10 @@ +{ + "culture": "ko", + "texts": { + "DocsTitle": "VoloDocs", + "WelcomeVoloDocs": "VoloDocs에 오신 것을 환영합니다!", + "NoProjectWarning": "아직 정의된 프로젝트가 없습니다!", + "CreateYourFirstProject": "첫 번째 프로젝트를 시작하려면 여기를 클릭하세요.", + "NoProject": "프로젝트가 없습니다!" + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/ko.json b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/ko.json new file mode 100644 index 0000000000..4be5125365 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/ko.json @@ -0,0 +1,74 @@ +{ + "culture": "ko", + "texts": { + "Permission:DocumentManagement": "문서 관리", + "Permission:Projects": "프로젝트", + "Permission:Edit": "편집", + "Permission:Delete": "삭제", + "Permission:Create": "생성", + "Permission:Documents": "문서", + "Menu:Documents": "문서", + "Menu:DocumentManagement": "문서", + "Menu:ProjectManagement": "프로젝트", + "CreateANewProject": "새 프로젝트 생성", + "Edit": "편집", + "Create": "생성", + "Pull": "가져오기", + "Projects": "프로젝트", + "Name": "이름", + "ShortName": "단축 이름", + "DocumentStoreType": "문서 저장소 유형", + "Format": "형식", + "ShortNameInfoText": "고유 URL에 사용됩니다.", + "DisplayName:Name": "이름", + "DisplayName:ShortName": "단축 이름", + "DisplayName:Format": "형식", + "DisplayName:DefaultDocumentName": "기본 문서 이름", + "DisplayName:NavigationDocumentName": "탐색 문서 이름", + "DisplayName:MinimumVersion": "최소 버전", + "DisplayName:MainWebsiteUrl": "메인 웹사이트 URL", + "DisplayName:LatestVersionBranchName": "최신 버전 브랜치 이름", + "DisplayName:GitHubRootUrl": "GitHub 루트 URL", + "DisplayName:GitHubAccessToken": "GitHub 액세스 토큰", + "DisplayName:GitHubUserAgent": "GitHub 사용자 에이전트", + "DisplayName:GithubVersionProviderSource": "GitHub 버전 공급자 소스", + "DisplayName:VersionBranchPrefix": "버전 브랜치 접두사", + "DisplayName:All": "모두 가져오기", + "DisplayName:LanguageCode": "언어 코드", + "DisplayName:Version": "버전", + "Documents": "문서", + "RemoveFromCache": "캐시에서 제거하고 재인덱싱", + "Reindex": "재인덱싱", + "ReindexCompleted": "재인덱싱 완료", + "RemovedFromCache": "캐시에서 제거하고 재인덱싱했습니다.", + "RemoveFromCacheConfirmation": "이 항목을 캐시에서 제거하시겠습니까?", + "ReIndexDocumentConfirmation": "문서 \"{0}\"을(를) 재인덱싱하시겠습니까?", + "DeleteFromDatabase": "데이터베이스에서 삭제", + "Deleted": "삭제됨", + "Search": "검색", + "StartDate": "시작일", + "EndDate": "종료일", + "CreationTime": "생성 시간", + "LastUpdateTime": "마지막 업데이트", + "LastSignificantUpdateTime": "마지막 주요 업데이트", + "Version": "버전", + "LanguageCode": "언어 코드", + "FileName": "파일 이름", + "LastCachedTime": "캐시 시간", + "Project": "프로젝트", + "AdvancedFilters": "고급 필터", + "RemoveCacheAndReIndexConfirmation": "문서 \"{0}\"을(를) 캐시에서 제거하고 재인덱싱합니다. 계속하시겠습니까?", + "GeneratePdf": "PDF 생성", + "Generating": "생성 중...", + "Language": "언어", + "ForceToGenerateNewPdf": "새 PDF 강제 생성", + "PdfGeneratedSuccessfully": "PDF가 생성되었습니다.", + "GenerateAndDownloadPdf": "PDF 생성 및 다운로드", + "PdfFileDeletionWarningMessage": "PDF 파일 \"{0}\"을(를) 삭제하시겠습니까?", + "ManagePdfFiles": "PDF 파일 관리", + "Permission:ManagePdfFiles": "PDF 파일 관리", + "PdfDeletedSuccessfully": "PDF 파일이 삭제되었습니다.", + "PdfGenerationStarted": "PDF 생성을 시작했습니다.", + "PdfGenerationStartedInfoMessage": "PDF 생성을 시작했습니다. 완료되면 PDF 파일 섹션에 파일이 추가되었는지 확인할 수 있습니다." + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/Localization/Resources/Docs/ApplicationContracts/ko.json b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/Localization/Resources/Docs/ApplicationContracts/ko.json new file mode 100644 index 0000000000..e110342281 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application.Contracts/Volo/Docs/Common/Localization/Resources/Docs/ApplicationContracts/ko.json @@ -0,0 +1,7 @@ +{ + "culture": "ko", + "texts": { + "Permission:DocumentManagement.Common": "문서 관리 공통", + "Permission:PdfDownload": "PDF 다운로드" + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ko.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ko.json new file mode 100644 index 0000000000..7e6d22155b --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ko.json @@ -0,0 +1,51 @@ +{ + "culture": "ko", + "texts": { + "Documents": "문서", + "BackToWebsite": "웹사이트로 돌아가기", + "Contributors": "기여자", + "ShareOn": "공유", + "Version": "버전", + "Edit": "편집", + "LastEditTime": "마지막 편집", + "Delete": "삭제", + "ClearCache": "캐시 지우기", + "ClearCacheConfirmationMessage": "프로젝트 \"{0}\"의 모든 캐시를 지우시겠습니까?", + "ReIndexAllProjects": "모든 프로젝트 다시 인덱싱", + "ReIndexProject": "프로젝트 다시 인덱싱", + "ReIndexProjectConfirmationMessage": "프로젝트 \"{0}\"을(를) 다시 인덱싱하시겠습니까?", + "SuccessfullyReIndexProject": "다시 인덱싱했습니다: \"{0}\"", + "ReIndexAllProjectConfirmationMessage": "모든 프로젝트를 다시 인덱싱하시겠습니까?", + "SuccessfullyReIndexAllProject": "모든 프로젝트를 다시 인덱싱했습니다.", + "InThisDocument": "이 문서에서", + "GoToTop": "맨 위로 이동", + "Projects": "프로젝트", + "NoProjectWarning": "아직 프로젝트가 없습니다!", + "DocumentNotFound": "요청한 문서를 찾을 수 없습니다!", + "ProjectNotFound": "요청한 프로젝트를 찾을 수 없습니다!", + "NavigationDocumentNotFound": "이 버전에는 탐색 문서가 없습니다!", + "DocumentNotFoundInSelectedLanguage": "요청한 언어의 문서를 찾을 수 없어 기본 언어의 문서를 표시합니다.", + "FilterTopics": "주제 필터링", + "FullSearch": "문서에서 검색", + "Volo.Docs.Domain:010001": "Elasticsearch가 활성화되어 있지 않습니다.", + "MultipleVersionDocumentInfo": "이 문서에는 여러 버전이 있습니다. 가장 적합한 옵션을 선택하세요.", + "DocumentOptions": "문서 옵션", + "New": "신규", + "Upd": "업데이트", + "NewExplanation": "최근 2주 이내에 생성되었습니다.", + "UpdatedExplanation": "최근 2주 이내에 업데이트되었습니다.", + "Volo.Docs.Domain:010002": "단축 이름 {ShortName}이(가) 이미 존재합니다.", + "Preview": "미리 보기", + "Search": "검색", + "SearchResults": "검색 결과", + "SearchInTheAllDocuments": "모든 문서에서 검색", + "Next": "다음", + "Previous": "이전", + "ProjectDeletionWarningMessage": "프로젝트가 삭제됩니다.", + "Docs_Page_Title": "ABP 문서", + "Docs_Page_Description": "ABP 문서에서 포괄적인 가이드와 API 참조를 확인하여 개발 및 문제 해결에 활용할 수 있습니다.", + "GoogleTranslate": "Google 번역", + "DownloadPDF": "PDF 다운로드", + "PdfFileGeneratedSuccessfully": "PDF 파일이 생성되었습니다." + } +} 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 e031837165..91b9841a65 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 @@ -10,7 +10,6 @@ @using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Packages.Anchor @using Volo.Abp.AspNetCore.Mvc.UI.Packages.Clipboard -@using Volo.Abp.AspNetCore.Mvc.UI.Packages.MalihuCustomScrollbar @using Volo.Abp.AspNetCore.Mvc.UI.Packages.Popper @using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs @using Volo.Abp.AspNetCore.Mvc.UI.Theming @@ -38,7 +37,6 @@ @section styles { - @if (DocsUiOptions.Value.EnableEnlargeImage) @@ -64,7 +62,6 @@ } - @@ -306,7 +303,7 @@
-
diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml index b60978f32e..bef5e53a1e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml @@ -1,4 +1,5 @@ @page +@using Localization.Resources.AbpUi @using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @@ -10,6 +11,7 @@ @using Volo.Abp.Identity @model EditModalModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer UiL @inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; @@ -30,7 +32,7 @@
-
diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js index fa63b96af4..9445cd33c8 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js @@ -11,12 +11,9 @@ return; } - if(passwordInput.attr("type") === "password") { - passwordInput.attr("type", "text"); - } - else { - passwordInput.attr("type", "password"); - } + var isRevealing = passwordInput.attr("type") === "password"; + passwordInput.attr("type", isRevealing ? "text" : "password"); + button.attr("aria-pressed", isRevealing); var icon = button.find("i"); if(icon) { diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ko.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ko.json new file mode 100644 index 0000000000..949706d2b0 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/ko.json @@ -0,0 +1,17 @@ +{ + "culture": "ko", + "texts": { + "Volo.IdentityServer:DuplicateIdentityResourceName": "ID 리소스 이름이 이미 존재합니다: {Name}", + "Volo.IdentityServer:DuplicateApiResourceName": "API 리소스 이름이 이미 존재합니다: {Name}", + "Volo.IdentityServer:DuplicateApiScopeName": "API 범위 이름이 이미 존재합니다: {Name}", + "Volo.IdentityServer:DuplicateClientId": "클라이언트 ID가 이미 존재합니다: {ClientId}", + "UserLockedOut": "잘못된 로그인 시도로 인해 사용자 계정이 잠겼습니다. 잠시 후 다시 시도해 주세요.", + "InvalidUserNameOrPassword": "사용자 이름 또는 비밀번호가 올바르지 않습니다!", + "LoginIsNotAllowed": "로그인할 수 없습니다! 계정이 비활성 상태이거나 이메일 또는 전화번호 확인이 필요합니다.", + "InvalidUsername": "사용자 이름 또는 비밀번호가 올바르지 않습니다!", + "InvalidAuthenticatorCode": "인증 코드가 올바르지 않습니다!", + "InvalidRecoveryCode": "복구 코드가 올바르지 않습니다!", + "TheTargetUserIsNotLinkedToYou": "대상 사용자가 현재 사용자와 연결되어 있지 않습니다!", + "ClientResourcePermissionProviderKeyLookupService": "클라이언트" + } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ko.json b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ko.json new file mode 100644 index 0000000000..5142cfcf44 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Localization/OpenIddict/ko.json @@ -0,0 +1,16 @@ +{ + "culture": "ko", + "texts": { + "TheOpenIDConnectRequestCannotBeRetrieved": "OpenID Connect 요청을 가져올 수 없습니다.", + "TheUserDetailsCannotBbeRetrieved": "사용자 세부 정보를 가져올 수 없습니다.", + "TheApplicationDetailsCannotBeFound": "애플리케이션 세부 정보를 찾을 수 없습니다.", + "DetailsConcerningTheCallingClientApplicationCannotBeFound": "호출한 클라이언트 애플리케이션의 세부 정보를 찾을 수 없습니다.", + "TheSpecifiedGrantTypeIsNotImplemented": "지정된 권한 부여 유형 {0}은(는) 구현되어 있지 않습니다.", + "Authorization": "권한 부여", + "DoYouWantToGrantAccessToYourData": "{0}에게 데이터 접근 권한을 부여하시겠습니까?", + "ScopesRequested": "요청된 범위", + "Accept": "허용", + "Deny": "거부", + "ApplicationResourcePermissionProviderKeyLookupService": "클라이언트" + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ko.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ko.json new file mode 100644 index 0000000000..eea6682bd4 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ko.json @@ -0,0 +1,29 @@ +{ + "culture": "ko", + "texts": { + "Permissions": "권한", + "OnlyProviderPermissons": "이 프로바이더만", + "All": "모두", + "SelectAllInAllTabs": "모든 권한 부여", + "SelectAllInThisTab": "모두 선택", + "SaveWithoutAnyPermissionsWarningMessage": "권한을 하나도 부여하지 않고 저장하시겠습니까?", + "PermissionGroup": "권한 그룹", + "Filter": "필터", + "ResourcePermissions": "권한", + "ResourcePermissionTarget": "대상", + "ResourcePermissionPermissions": "권한", + "AddResourcePermission": "권한 추가", + "ResourcePermissionDeletionConfirmationMessage": "모든 권한을 삭제하시겠습니까?", + "UpdateResourcePermission": "권한 업데이트", + "GrantAllResourcePermissions": "모두 부여", + "NoResourceProviderKeyLookupServiceFound": "프로바이더 키 조회 서비스를 찾을 수 없습니다.", + "NoResourcePermissionFound": "정의된 권한이 없습니다.", + "UpdatePermission": "권한 업데이트", + "NoPermissionsAssigned": "할당된 권한이 없습니다.", + "SelectProvider": "프로바이더 선택", + "SearchProviderKey": "프로바이더 키 검색", + "Provider": "프로바이더", + "ErrorLoadingPermissions": "권한을 불러오는 중 오류가 발생했습니다.", + "PleaseSelectProviderAndPermissions": "프로바이더와 권한을 선택해 주세요." + } +} \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js index 47cacfd2db..ac3c3b9fdf 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js @@ -295,15 +295,6 @@ var abp = abp || {}; }); }); - $(function () { - $('.custom-scroll-content').mCustomScrollbar({ - theme: 'minimal-dark', - }); - $('.custom-scroll-container > .col-4').mCustomScrollbar({ - theme: 'minimal-dark', - }); - }); - initSelectAllInThisTab(); setSelectAllInAllTabs(); diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ko.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ko.json new file mode 100644 index 0000000000..b5b2366535 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ko.json @@ -0,0 +1,43 @@ +{ + "culture": "ko", + "texts": { + "Settings": "설정", + "SavedSuccessfully": "성공적으로 저장되었습니다.", + "Permission:SettingManagement": "설정 관리", + "Permission:Emailing": "이메일", + "Permission:EmailingTest": "이메일 테스트", + "Permission:TimeZone": "시간대", + "SendTestEmail": "테스트 이메일 보내기", + "SenderEmailAddress": "발신자 이메일 주소", + "TargetEmailAddress": "수신자 이메일 주소", + "Subject": "제목", + "Body": "본문", + "TestEmailSubject": "테스트 이메일 {0}", + "TestEmailBody": "테스트 이메일 본문 메시지입니다.", + "SentSuccessfully": "성공적으로 전송되었습니다.", + "MailSendingFailed": "이메일 전송에 실패했습니다. 이메일 설정을 확인한 후 다시 시도해 주세요.", + "Send": "보내기", + "Menu:Settings": "설정", + "Menu:Emailing": "이메일", + "Menu:TimeZone": "시간대", + "DisplayName:Timezone": "시간대", + "TimezoneHelpText": "이 기능을 사용하면 서버의 기본 시간대를 설정할 수 있으며, 사용자는 각자 자신의 시간대를 선택할 수 있습니다. 사용자의 시간대가 서버 시간대와 다르면 모든 시간이 이에 맞게 조정됩니다. 예를 들어 서버가 Europe/London(00:00)으로 설정되어 있고 사용자가 Europe/Paris(+01:00)에 있는 경우, 해당 사용자의 시간은 1시간 조정됩니다. '기본 시간대'를 선택하면 서버 또는 브라우저의 시간대가 자동으로 사용됩니다.", + "DefaultTimeZone": "기본 시간대", + "SmtpHost": "호스트", + "SmtpPort": "포트", + "SmtpUserName": "사용자 이름", + "SmtpPassword": "비밀번호", + "SmtpDomain": "도메인", + "SmtpEnableSsl": "SSL 사용", + "SmtpUseDefaultCredentials": "기본 자격 증명 사용", + "DefaultFromAddress": "기본 발신자 주소", + "DefaultFromDisplayName": "기본 발신자 표시 이름", + "Feature:SettingManagementGroup": "설정 관리", + "Feature:SettingManagementEnable": "설정 관리 사용", + "Feature:SettingManagementEnableDescription": "애플리케이션에서 설정 관리 시스템을 사용합니다.", + "Feature:AllowChangingEmailSettings": "이메일 설정 변경 허용", + "Feature:AllowChangingEmailSettingsDescription": "이메일 설정 변경을 허용합니다.", + "SmtpPasswordPlaceholder": "비밀번호를 변경하려면 값을 입력하세요.", + "NoSettingsAvailable": "표시할 설정이 없습니다." + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor index e2211914f4..26e7969ade 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor.MudBlazor/Pages/TenantManagement/TenantManagement.razor @@ -48,14 +48,18 @@ Required="true" RequiredError="@L["ThisFieldIsRequired."]" InputType="InputType.Email" /> - +
+ + +
diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor index 49e4c64822..57b9b7844f 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor @@ -77,8 +77,9 @@ - diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ko.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ko.json new file mode 100644 index 0000000000..13bfe0999b --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ko.json @@ -0,0 +1,23 @@ +{ + "culture": "ko", + "texts": { + "Volo.Abp.TenantManagement:DuplicateTenantName": "테넌트 이름이 이미 존재합니다: {Name}", + "Menu:TenantManagement": "테넌트 관리", + "Tenants": "테넌트", + "NewTenant": "새 테넌트", + "TenantName": "테넌트 이름", + "DisplayName:TenantName": "테넌트 이름", + "TenantDeletionConfirmationMessage": "테넌트 '{0}'이(가) 삭제됩니다. 계속하시겠습니까?", + "ConnectionStrings": "연결 문자열", + "DisplayName:DefaultConnectionString": "기본 연결 문자열", + "DisplayName:UseSharedDatabase": "공유 데이터베이스 사용", + "Permission:TenantManagement": "테넌트 관리", + "Permission:Create": "생성", + "Permission:Edit": "편집", + "Permission:Delete": "삭제", + "Permission:ManageConnectionStrings": "연결 문자열 관리", + "Permission:ManageFeatures": "기능", + "DisplayName:AdminEmailAddress": "관리자 이메일 주소", + "DisplayName:AdminPassword": "관리자 비밀번호" + } +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Create.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Create.js index b19f44be46..8a8c8bc859 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Create.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Create.js @@ -6,12 +6,9 @@ $(document).ready(function () { return; } - if (passwordInput.attr("type") === "password") { - passwordInput.attr("type", "text"); - } - else { - passwordInput.attr("type", "password"); - } + let isRevealing = passwordInput.attr("type") === "password"; + passwordInput.attr("type", isRevealing ? "text" : "password"); + button.attr("aria-pressed", isRevealing); let icon = button.find("i"); if (icon) { diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml index bcbda372e2..bb4ec71f3c 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml @@ -1,4 +1,5 @@ @page +@using Localization.Resources.AbpUi @using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @@ -10,6 +11,7 @@ @using Volo.Abp.TenantManagement; @model CreateModalModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer UiL @inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; @@ -27,7 +29,7 @@ *
-
diff --git a/modules/virtual-file-explorer/src/Volo.Abp.VirtualFileExplorer.Contracts/Volo/Abp/VirtualFileExplorer/Localization/Resources/ko.json b/modules/virtual-file-explorer/src/Volo.Abp.VirtualFileExplorer.Contracts/Volo/Abp/VirtualFileExplorer/Localization/Resources/ko.json new file mode 100644 index 0000000000..8b53474d20 --- /dev/null +++ b/modules/virtual-file-explorer/src/Volo.Abp.VirtualFileExplorer.Contracts/Volo/Abp/VirtualFileExplorer/Localization/Resources/ko.json @@ -0,0 +1,16 @@ +{ + "culture": "ko", + "texts": { + "VirtualFileExplorer": "가상 파일 탐색기", + "VirtualFileType": "가상 파일 형식", + "Menu:VirtualFileExplorer": "가상 파일 탐색기", + "LastUpdateTime": "마지막 업데이트 시간", + "VirtualFileName": "가상 파일 이름", + "FileContent": "파일 내용", + "Size": "크기", + "BackToRoot": "루트로 돌아가기", + "EmptyFileInfoList": "가상 파일이 없습니다.", + "Permission:AbpVirtualFileExplorer": "가상 파일 탐색기", + "Permission:AbpVirtualFileExplorer:View": "보기" + } +} diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 8eccea1bb5..4cdf59361e 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -51,29 +51,29 @@ "@abp/ng.theme.lepton-x": "~5.7.0-rc.4", "@abp/ng.schematics": "~10.7.0-rc.4", "@abp/utils": "~10.7.0-rc.4", - "@angular-devkit/build-angular": "~22.0.0", - "@angular-devkit/core": "~22.0.0", - "@angular-devkit/schematics": "~22.0.0", - "@angular-devkit/schematics-cli": "~22.0.0", - "@angular-eslint/eslint-plugin": "~22.0.0", - "@angular-eslint/eslint-plugin-template": "~22.0.0", - "@angular-eslint/template-parser": "~22.0.0", - "@angular/animations": "~22.0.0", - "@angular/aria": "~22.0.0", - "@angular/build": "~22.0.0", - "@angular/cli": "~22.0.0", - "@angular/common": "~22.0.0", - "@angular/compiler": "~22.0.0", - "@angular/compiler-cli": "~22.0.0", - "@angular/core": "~22.0.0", - "@angular/forms": "~22.0.0", - "@angular/language-service": "~22.0.0", - "@angular/localize": "~22.0.0", - "@angular/platform-browser": "~22.0.0", - "@angular/platform-browser-dynamic": "~22.0.0", - "@angular/platform-server": "~22.0.0", - "@angular/router": "~22.0.0", - "@angular/ssr": "~22.0.0", + "@angular-devkit/build-angular": "~22.1.0", + "@angular-devkit/core": "~22.1.0", + "@angular-devkit/schematics": "~22.1.0", + "@angular-devkit/schematics-cli": "~22.1.0", + "@angular-eslint/eslint-plugin": "~22.1.0", + "@angular-eslint/eslint-plugin-template": "~22.1.0", + "@angular-eslint/template-parser": "~22.1.0", + "@angular/animations": "~22.1.0", + "@angular/aria": "~22.1.0", + "@angular/build": "~22.1.0", + "@angular/cli": "~22.1.0", + "@angular/common": "~22.1.0", + "@angular/compiler": "~22.1.0", + "@angular/compiler-cli": "~22.1.0", + "@angular/core": "~22.1.0", + "@angular/forms": "~22.1.0", + "@angular/language-service": "~22.1.0", + "@angular/localize": "~22.1.0", + "@angular/platform-browser": "~22.1.0", + "@angular/platform-browser-dynamic": "~22.1.0", + "@angular/platform-server": "~22.1.0", + "@angular/router": "~22.1.0", + "@angular/ssr": "~22.1.0", "@fortawesome/fontawesome-free": "^6.0.0", "@ng-bootstrap/ng-bootstrap": "~20.0.0", "@ngneat/spectator": "~19.6.2", @@ -91,7 +91,7 @@ "@nx/web": "~22.7.5", "@nx/workspace": "~22.7.5", "@popperjs/core": "~2.11.0", - "@schematics/angular": "~22.0.0", + "@schematics/angular": "~22.1.0", "@swc-node/register": "~1.11.1", "@swc/cli": "~0.7.10", "@swc/core": "~1.15.8", @@ -129,7 +129,7 @@ "just-compare": "^2.0.0", "lerna": "^4.0.0", "lint-staged": "^13.0.0", - "ng-packagr": "~22.0.0", + "ng-packagr": "~22.1.0", "ng-zorro-antd": "~21.1.0", "nx": "~22.7.5", "postcss": "^8.0.0", diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index b12782288d..d9ad09c780 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -52,7 +52,9 @@ export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit if (!this.rendered) { this.cdrSubject.next(); } else { - this.cdRef.detectChanges(); + // Defer via queue — sync detectChanges during ApplicationRef.tick + // (e.g. post-login permission/config refresh) can contribute to NG0101. + this.queue.add(() => this.cdRef.detectChanges()); } } else { this.cdRef.markForCheck(); diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts index 670666a1c9..b91d15ee4e 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/http/modeling/models.ts @@ -82,6 +82,8 @@ export interface PropertyApiDescriptionModel { maxLength?: number; minimum?: string; maximum?: string; + minimumIsExclusive?: boolean; + maximumIsExclusive?: boolean; regex?: string; } diff --git a/npm/ng-packs/packages/feature-management/package.json b/npm/ng-packs/packages/feature-management/package.json index 613ef145b1..7861f5262d 100644 --- a/npm/ng-packs/packages/feature-management/package.json +++ b/npm/ng-packs/packages/feature-management/package.json @@ -11,7 +11,7 @@ "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/aria": "~21.2.0" + "@angular/aria": "~22.1.0" }, "publishConfig": { "access": "public" diff --git a/npm/ng-packs/packages/permission-management/package.json b/npm/ng-packs/packages/permission-management/package.json index da17790c96..ce89550a71 100644 --- a/npm/ng-packs/packages/permission-management/package.json +++ b/npm/ng-packs/packages/permission-management/package.json @@ -11,7 +11,7 @@ "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/aria": "~21.2.0" + "@angular/aria": "~22.1.0" }, "publishConfig": { "access": "public" diff --git a/npm/ng-packs/packages/schematics/package.json b/npm/ng-packs/packages/schematics/package.json index 0daa9d35d6..426868576b 100644 --- a/npm/ng-packs/packages/schematics/package.json +++ b/npm/ng-packs/packages/schematics/package.json @@ -4,16 +4,16 @@ "author": "", "schematics": "./collection.json", "dependencies": { - "@angular-devkit/core": "~22.0.0", - "@angular-devkit/schematics": "~22.0.0", - "@angular/cli": "~22.0.0", + "@angular-devkit/core": "~22.1.0", + "@angular-devkit/schematics": "~22.1.0", + "@angular/cli": "~22.1.0", "got": "^11.5.2", "jsonc-parser": "^2.3.0", "should-quote": "^1.0.0", "typescript": "~6.0.3" }, "devDependencies": { - "@schematics/angular": "~22.0.0", + "@schematics/angular": "~22.1.0", "@types/jest": "29.4.4", "@types/node": "20.2.5", "jest": "29.4.3", diff --git a/npm/ng-packs/packages/setting-management/package.json b/npm/ng-packs/packages/setting-management/package.json index 630fced7f2..9df1df1883 100644 --- a/npm/ng-packs/packages/setting-management/package.json +++ b/npm/ng-packs/packages/setting-management/package.json @@ -12,7 +12,7 @@ "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/aria": "~21.2.0" + "@angular/aria": "~22.1.0" }, "publishConfig": { "access": "public" diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html index b23fdc598e..1eabd84821 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html @@ -1 +1 @@ - + diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts index b8a86342b1..884eb9db99 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts @@ -6,7 +6,7 @@ import { SubscriptionService, TreeNode, } from '@abp/ng.core'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core'; import { Router } from '@angular/router'; import { map, startWith } from 'rxjs/operators'; import { eThemeSharedRouteNames } from '../../enums/route-names'; @@ -20,14 +20,13 @@ import { BreadcrumbItemsComponent } from '../breadcrumb-items/breadcrumb-items.c imports: [BreadcrumbItemsComponent], }) export class BreadcrumbComponent implements OnInit { - readonly cdRef = inject(ChangeDetectorRef); private router = inject(Router); private routes = inject(RoutesService); private subscription = inject(SubscriptionService); private routerEvents = inject(RouterEvents); private routeCultureUrl = inject(RouteBasedCultureUrlService); - segments: Partial[] = []; + readonly segments = signal[]>([]); ngOnInit(): void { this.subscription.addOne( @@ -38,18 +37,18 @@ export class BreadcrumbComponent implements OnInit { ), ), route => { - this.segments = []; + const next: Partial[] = []; if (route) { let node = { parent: route } as TreeNode; while (node.parent) { node = node.parent; const { parent, children, isLeaf, ...segment } = node; - if (!isAdministration(segment)) this.segments.unshift(segment); + if (!isAdministration(segment)) next.unshift(segment); } - - this.cdRef.detectChanges(); } + + this.segments.set(next); }, ); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts index b6db6e7716..f1f3a5c114 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts @@ -1,4 +1,4 @@ -import { ApplicationRef, ComponentRef, inject, Injectable } from '@angular/core'; +import { ComponentRef, inject, Injectable } from '@angular/core'; import { ContentProjectionService, LocalizationParam, @@ -12,7 +12,6 @@ import { Toaster } from '../models'; providedIn: 'root', }) export class ToasterService implements ToasterContract { - private readonly appRef = inject(ApplicationRef); private readonly contentProjectionService = inject(ContentProjectionService); private lastId = -1; @@ -37,8 +36,9 @@ export class ToasterService implements ToasterContract { } this.containerComponentRef.instance.setToasts(this.toasts); + // Only refresh the projected toast host. Calling ApplicationRef.tick() here + // races with zone-driven CD and throws NG0101 (recursive tick). this.containerComponentRef.changeDetectorRef.detectChanges(); - this.appRef.tick(); } /** diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts index 6ad217d3b8..ce1463e2cf 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts @@ -135,7 +135,7 @@ describe('BreadcrumbComponent', () => { const breadcrumb = spectator.fixture.debugElement.query(By.directive(BreadcrumbComponent)) .componentInstance as BreadcrumbComponent; - expect(breadcrumb.segments.map(segment => segment.path)).toEqual([ + expect(breadcrumb.segments().map(segment => segment.path)).toEqual([ '/identity', '/identity/users', ]); diff --git a/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json b/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json index 9f7192c8ac..db9d122025 100644 --- a/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json +++ b/npm/packs/aspnetcore.mvc.ui.theme.shared/package.json @@ -19,7 +19,6 @@ "@abp/jquery-validation-unobtrusive": "~10.7.0-rc.4", "@abp/lodash": "~10.7.0-rc.4", "@abp/luxon": "~10.7.0-rc.4", - "@abp/malihu-custom-scrollbar-plugin": "~10.7.0-rc.4", "@abp/moment": "~10.7.0-rc.4", "@abp/select2": "~10.7.0-rc.4", "@abp/sweetalert2": "~10.7.0-rc.4", diff --git a/npm/packs/blogging/package.json b/npm/packs/blogging/package.json index c5ed75691a..27b495df84 100644 --- a/npm/packs/blogging/package.json +++ b/npm/packs/blogging/package.json @@ -6,7 +6,6 @@ }, "dependencies": { "@abp/aspnetcore.mvc.ui.theme.shared": "~10.7.0-rc.4", - "@abp/owl.carousel": "~10.7.0-rc.4", "@abp/prismjs": "~10.7.0-rc.4", "@abp/tui-editor": "~10.7.0-rc.4" }, diff --git a/npm/packs/bootstrap-datepicker/abp.resourcemapping.js b/npm/packs/bootstrap-datepicker/abp.resourcemapping.js index 2db70fc044..ee879d3b39 100644 --- a/npm/packs/bootstrap-datepicker/abp.resourcemapping.js +++ b/npm/packs/bootstrap-datepicker/abp.resourcemapping.js @@ -1,6 +1,6 @@ module.exports = { mappings: { - "@node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js": "@libs/bootstrap-datepicker/", + "@node_modules/@abp/bootstrap-datepicker/src/bootstrap-datepicker.js": "@libs/bootstrap-datepicker/", "@node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css": "@libs/bootstrap-datepicker/", "@node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css.map": "@libs/bootstrap-datepicker/", "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" diff --git a/npm/packs/bootstrap-datepicker/package.json b/npm/packs/bootstrap-datepicker/package.json index b891181aec..5bde795201 100644 --- a/npm/packs/bootstrap-datepicker/package.json +++ b/npm/packs/bootstrap-datepicker/package.json @@ -10,7 +10,8 @@ "access": "public" }, "dependencies": { - "bootstrap-datepicker": "^1.10.1" + "bootstrap-datepicker": "1.10.1", + "jquery": "~4.0.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/bootstrap-datepicker/src/bootstrap-datepicker.js b/npm/packs/bootstrap-datepicker/src/bootstrap-datepicker.js new file mode 100644 index 0000000000..1772135e28 --- /dev/null +++ b/npm/packs/bootstrap-datepicker/src/bootstrap-datepicker.js @@ -0,0 +1,2048 @@ +/*! + * Datepicker for Bootstrap v1.10.1 (https://github.com/uxsolutions/bootstrap-datepicker) + * + * Licensed under the Apache License v2.0 (https://www.apache.org/licenses/LICENSE-2.0) + * + * Patched copy: uses Function.prototype.bind instead of the jQuery.proxy API deprecated in jQuery 4. + */ + +(function(factory){ + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function($, undefined){ + function UTCDate(){ + return new Date(Date.UTC.apply(Date, arguments)); + } + function UTCToday(){ + var today = new Date(); + return UTCDate(today.getFullYear(), today.getMonth(), today.getDate()); + } + function isUTCEquals(date1, date2) { + return ( + date1.getUTCFullYear() === date2.getUTCFullYear() && + date1.getUTCMonth() === date2.getUTCMonth() && + date1.getUTCDate() === date2.getUTCDate() + ); + } + function alias(method, deprecationMsg){ + return function(){ + if (deprecationMsg !== undefined) { + $.fn.datepicker.deprecated(deprecationMsg); + } + + return this[method].apply(this, arguments); + }; + } + function isValidDate(d) { + return d && !isNaN(d.getTime()); + } + + var DateArray = (function(){ + var extras = { + get: function(i){ + return this.slice(i)[0]; + }, + contains: function(d){ + // Array.indexOf is not cross-browser; + // $.inArray doesn't work with Dates + var val = d && d.valueOf(); + for (var i=0, l=this.length; i < l; i++) + // Use date arithmetic to allow dates with different times to match + if (0 <= this[i].valueOf() - val && this[i].valueOf() - val < 1000*60*60*24) + return i; + return -1; + }, + remove: function(i){ + this.splice(i,1); + }, + replace: function(new_array){ + if (!new_array) + return; + if (!Array.isArray(new_array)) + new_array = [new_array]; + this.clear(); + this.push.apply(this, new_array); + }, + clear: function(){ + this.length = 0; + }, + copy: function(){ + var a = new DateArray(); + a.replace(this); + return a; + } + }; + + return function(){ + var a = []; + a.push.apply(a, arguments); + $.extend(a, extras); + return a; + }; + })(); + + + // Picker object + + var Datepicker = function(element, options){ + $.data(element, 'datepicker', this); + + this._events = []; + this._secondaryEvents = []; + + this._process_options(options); + + this.dates = new DateArray(); + this.viewDate = this.o.defaultViewDate; + this.focusDate = null; + + this.element = $(element); + this.isInput = this.element.is('input'); + this.inputField = this.isInput ? this.element : this.element.find('input'); + this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .input-group-append, .input-group-prepend, .btn') : false; + if (this.component && this.component.length === 0){ + this.component = false; + } + + if (this.o.isInline === null){ + this.isInline = !this.component && !this.isInput; + } else { + this.isInline = this.o.isInline; + } + + this.picker = $(DPGlobal.template); + + // Checking templates and inserting + if (this._check_template(this.o.templates.leftArrow)) { + this.picker.find('.prev').html(this.o.templates.leftArrow); + } + + if (this._check_template(this.o.templates.rightArrow)) { + this.picker.find('.next').html(this.o.templates.rightArrow); + } + + this._buildEvents(); + this._attachEvents(); + + if (this.isInline){ + this.picker.addClass('datepicker-inline').appendTo(this.element); + } + else { + this.picker.addClass('datepicker-dropdown dropdown-menu'); + } + + if (this.o.rtl){ + this.picker.addClass('datepicker-rtl'); + } + + if (this.o.calendarWeeks) { + this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear') + .attr('colspan', function(i, val){ + return Number(val) + 1; + }); + } + + this._process_options({ + startDate: this._o.startDate, + endDate: this._o.endDate, + daysOfWeekDisabled: this.o.daysOfWeekDisabled, + daysOfWeekHighlighted: this.o.daysOfWeekHighlighted, + datesDisabled: this.o.datesDisabled + }); + + this._allow_update = false; + this.setViewMode(this.o.startView); + this._allow_update = true; + + this.fillDow(); + this.fillMonths(); + + this.update(); + + if (this.isInline){ + this.show(); + } + }; + + Datepicker.prototype = { + constructor: Datepicker, + + _resolveViewName: function(view){ + $.each(DPGlobal.viewModes, function(i, viewMode){ + if (view === i || $.inArray(view, viewMode.names) !== -1){ + view = i; + return false; + } + }); + + return view; + }, + + _resolveDaysOfWeek: function(daysOfWeek){ + if (!Array.isArray(daysOfWeek)) + daysOfWeek = daysOfWeek.split(/[,\s]*/); + return $.map(daysOfWeek, Number); + }, + + _check_template: function(tmp){ + try { + // If empty + if (tmp === undefined || tmp === "") { + return false; + } + // If no html, everything ok + if ((tmp.match(/[<>]/g) || []).length <= 0) { + return true; + } + // Checking if html is fine + var jDom = $(tmp); + return jDom.length > 0; + } + catch (ex) { + return false; + } + }, + + _process_options: function(opts){ + // Store raw options for reference + this._o = $.extend({}, this._o, opts); + // Processed options + var o = this.o = $.extend({}, this._o); + + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + var lang = o.language; + if (!dates[lang]){ + lang = lang.split('-')[0]; + if (!dates[lang]) + lang = defaults.language; + } + o.language = lang; + + // Retrieve view index from any aliases + o.startView = this._resolveViewName(o.startView); + o.minViewMode = this._resolveViewName(o.minViewMode); + o.maxViewMode = this._resolveViewName(o.maxViewMode); + + // Check view is between min and max + o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView)); + + // true, false, or Number > 0 + if (o.multidate !== true){ + o.multidate = Number(o.multidate) || false; + if (o.multidate !== false) + o.multidate = Math.max(0, o.multidate); + } + o.multidateSeparator = String(o.multidateSeparator); + + o.weekStart %= 7; + o.weekEnd = (o.weekStart + 6) % 7; + + var format = DPGlobal.parseFormat(o.format); + if (o.startDate !== -Infinity){ + if (!!o.startDate){ + if (o.startDate instanceof Date) + o.startDate = this._local_to_utc(this._zero_time(o.startDate)); + else + o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear); + } + else { + o.startDate = -Infinity; + } + } + if (o.endDate !== Infinity){ + if (!!o.endDate){ + if (o.endDate instanceof Date) + o.endDate = this._local_to_utc(this._zero_time(o.endDate)); + else + o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear); + } + else { + o.endDate = Infinity; + } + } + + o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled||[]); + o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]); + + o.datesDisabled = o.datesDisabled||[]; + if (!Array.isArray(o.datesDisabled)) { + o.datesDisabled = o.datesDisabled.split(','); + } + o.datesDisabled = $.map(o.datesDisabled, function(d){ + return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear); + }); + + var plc = String(o.orientation).toLowerCase().split(/\s+/g), + _plc = o.orientation.toLowerCase(); + plc = $.grep(plc, function(word){ + return /^auto|left|right|top|bottom$/.test(word); + }); + o.orientation = {x: 'auto', y: 'auto'}; + if (!_plc || _plc === 'auto') + ; // no action + else if (plc.length === 1){ + switch (plc[0]){ + case 'top': + case 'bottom': + o.orientation.y = plc[0]; + break; + case 'left': + case 'right': + o.orientation.x = plc[0]; + break; + } + } + else { + _plc = $.grep(plc, function(word){ + return /^left|right$/.test(word); + }); + o.orientation.x = _plc[0] || 'auto'; + + _plc = $.grep(plc, function(word){ + return /^top|bottom$/.test(word); + }); + o.orientation.y = _plc[0] || 'auto'; + } + if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') { + o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear); + } else if (o.defaultViewDate) { + var year = o.defaultViewDate.year || new Date().getFullYear(); + var month = o.defaultViewDate.month || 0; + var day = o.defaultViewDate.day || 1; + o.defaultViewDate = UTCDate(year, month, day); + } else { + o.defaultViewDate = UTCToday(); + } + }, + _applyEvents: function(evs){ + for (var i=0, el, ch, ev; i < evs.length; i++){ + el = evs[i][0]; + if (evs[i].length === 2){ + ch = undefined; + ev = evs[i][1]; + } else if (evs[i].length === 3){ + ch = evs[i][1]; + ev = evs[i][2]; + } + el.on(ev, ch); + } + }, + _unapplyEvents: function(evs){ + for (var i=0, el, ev, ch; i < evs.length; i++){ + el = evs[i][0]; + if (evs[i].length === 2){ + ch = undefined; + ev = evs[i][1]; + } else if (evs[i].length === 3){ + ch = evs[i][1]; + ev = evs[i][2]; + } + el.off(ev, ch); + } + }, + _buildEvents: function(){ + var events = { + keyup: (function(e){ + if ($.inArray(e.keyCode, [27, 37, 39, 38, 40, 32, 13, 9]) === -1) + this.update(); + }).bind(this), + keydown: this.keydown.bind(this), + paste: this.paste.bind(this) + }; + + if (this.o.showOnFocus === true) { + events.focus = this.show.bind(this); + } + + if (this.isInput) { // single input + this._events = [ + [this.element, events] + ]; + } + // component: input + button + else if (this.component && this.inputField.length) { + this._events = [ + // For components that are not readonly, allow keyboard nav + [this.inputField, events], + [this.component, { + click: this.show.bind(this) + }] + ]; + } + else { + this._events = [ + [this.element, { + click: this.show.bind(this), + keydown: this.keydown.bind(this) + }] + ]; + } + this._events.push( + // Component: listen for blur on element descendants + [this.element, '*', { + blur: (function(e){ + this._focused_from = e.target; + }).bind(this) + }], + // Input: listen for blur on element + [this.element, { + blur: (function(e){ + this._focused_from = e.target; + }).bind(this) + }] + ); + + if (this.o.immediateUpdates) { + // Trigger input updates immediately on changed year/month + this._events.push([this.element, { + 'changeYear changeMonth': (function(e){ + this.update(e.date); + }).bind(this) + }]); + } + + this._secondaryEvents = [ + [this.picker, { + click: this.click.bind(this) + }], + [this.picker, '.prev, .next', { + click: this.navArrowsClick.bind(this) + }], + [this.picker, '.day:not(.disabled)', { + click: this.dayCellClick.bind(this) + }], + [$(window), { + resize: this.place.bind(this) + }], + [$(document), { + 'mousedown touchstart': (function(e){ + // Clicked outside the datepicker, hide it + if (!( + this.element.is(e.target) || + this.element.find(e.target).length || + this.picker.is(e.target) || + this.picker.find(e.target).length || + this.isInline + )){ + this.hide(); + } + }).bind(this) + }] + ]; + }, + _attachEvents: function(){ + this._detachEvents(); + this._applyEvents(this._events); + }, + _detachEvents: function(){ + this._unapplyEvents(this._events); + }, + _attachSecondaryEvents: function(){ + this._detachSecondaryEvents(); + this._applyEvents(this._secondaryEvents); + }, + _detachSecondaryEvents: function(){ + this._unapplyEvents(this._secondaryEvents); + }, + _trigger: function(event, altdate){ + var date = altdate || this.dates.get(-1), + local_date = this._utc_to_local(date); + + this.element.trigger({ + type: event, + date: local_date, + viewMode: this.viewMode, + dates: $.map(this.dates, this._utc_to_local), + format: (function(ix, format){ + if (arguments.length === 0){ + ix = this.dates.length - 1; + format = this.o.format; + } else if (typeof ix === 'string'){ + format = ix; + ix = this.dates.length - 1; + } + format = format || this.o.format; + var date = this.dates.get(ix); + return DPGlobal.formatDate(date, format, this.o.language); + }).bind(this) + }); + }, + + show: function(){ + if (this.inputField.is(':disabled') || (this.inputField.prop('readonly') && this.o.enableOnReadonly === false)) + return; + if (!this.isInline) + this.picker.appendTo(this.o.container); + this.place(); + this.picker.show(); + this._attachSecondaryEvents(); + this._trigger('show'); + if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) { + $(this.element).blur(); + } + return this; + }, + + hide: function(){ + if (this.isInline || !this.picker.is(':visible')) + return this; + this.focusDate = null; + this.picker.hide().detach(); + this._detachSecondaryEvents(); + this.setViewMode(this.o.startView); + + if (this.o.forceParse && this.inputField.val()) + this.setValue(); + this._trigger('hide'); + return this; + }, + + destroy: function(){ + this.hide(); + this._detachEvents(); + this._detachSecondaryEvents(); + this.picker.remove(); + delete this.element.data().datepicker; + if (!this.isInput){ + delete this.element.data().date; + } + return this; + }, + + paste: function(e){ + var dateString; + if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types + && $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) { + dateString = e.originalEvent.clipboardData.getData('text/plain'); + } else if (window.clipboardData) { + dateString = window.clipboardData.getData('Text'); + } else { + return; + } + this.setDate(dateString); + this.update(); + e.preventDefault(); + }, + + _utc_to_local: function(utc){ + if (!utc) { + return utc; + } + + var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000)); + + if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) { + local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000)); + } + + return local; + }, + _local_to_utc: function(local){ + return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000)); + }, + _zero_time: function(local){ + return local && new Date(local.getFullYear(), local.getMonth(), local.getDate()); + }, + _zero_utc_time: function(utc){ + return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()); + }, + + getDates: function(){ + return $.map(this.dates, this._utc_to_local); + }, + + getUTCDates: function(){ + return $.map(this.dates, function(d){ + return new Date(d); + }); + }, + + getDate: function(){ + return this._utc_to_local(this.getUTCDate()); + }, + + getUTCDate: function(){ + var selected_date = this.dates.get(-1); + if (selected_date !== undefined) { + return new Date(selected_date); + } else { + return null; + } + }, + + clearDates: function(){ + this.inputField.val(''); + this._trigger('changeDate'); + this.update(); + if (this.o.autoclose) { + this.hide(); + } + }, + + setDates: function(){ + var args = Array.isArray(arguments[0]) ? arguments[0] : arguments; + this.update.apply(this, args); + this._trigger('changeDate'); + this.setValue(); + return this; + }, + + setUTCDates: function(){ + var args = Array.isArray(arguments[0]) ? arguments[0] : arguments; + this.setDates.apply(this, $.map(args, this._utc_to_local)); + return this; + }, + + setDate: alias('setDates'), + setUTCDate: alias('setUTCDates'), + remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead'), + + setValue: function(){ + var formatted = this.getFormattedDate(); + this.inputField.val(formatted); + return this; + }, + + getFormattedDate: function(format){ + if (format === undefined) + format = this.o.format; + + var lang = this.o.language; + return $.map(this.dates, function(d){ + return DPGlobal.formatDate(d, format, lang); + }).join(this.o.multidateSeparator); + }, + + getStartDate: function(){ + return this.o.startDate; + }, + + setStartDate: function(startDate){ + this._process_options({startDate: startDate}); + this.update(); + this.updateNavArrows(); + return this; + }, + + getEndDate: function(){ + return this.o.endDate; + }, + + setEndDate: function(endDate){ + this._process_options({endDate: endDate}); + this.update(); + this.updateNavArrows(); + return this; + }, + + setDaysOfWeekDisabled: function(daysOfWeekDisabled){ + this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); + this.update(); + return this; + }, + + setDaysOfWeekHighlighted: function(daysOfWeekHighlighted){ + this._process_options({daysOfWeekHighlighted: daysOfWeekHighlighted}); + this.update(); + return this; + }, + + setDatesDisabled: function(datesDisabled){ + this._process_options({datesDisabled: datesDisabled}); + this.update(); + return this; + }, + + place: function(){ + if (this.isInline) + return this; + var calendarWidth = this.picker.outerWidth(), + calendarHeight = this.picker.outerHeight(), + visualPadding = 10, + container = $(this.o.container), + windowWidth = container.width(), + scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(), + appendOffset = container.offset(); + + var parentsZindex = [0]; + this.element.parents().each(function(){ + var itemZIndex = $(this).css('z-index'); + if (itemZIndex !== 'auto' && Number(itemZIndex) !== 0) parentsZindex.push(Number(itemZIndex)); + }); + var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset; + var offset = this.component ? this.component.parent().offset() : this.element.offset(); + var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false); + var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false); + var left = offset.left - appendOffset.left; + var top = offset.top - appendOffset.top; + + if (this.o.container !== 'body') { + top += scrollTop; + } + + this.picker.removeClass( + 'datepicker-orient-top datepicker-orient-bottom '+ + 'datepicker-orient-right datepicker-orient-left' + ); + + if (this.o.orientation.x !== 'auto'){ + this.picker.addClass('datepicker-orient-' + this.o.orientation.x); + if (this.o.orientation.x === 'right') + left -= calendarWidth - width; + } + // auto x orientation is best-placement: if it crosses a window + // edge, fudge it sideways + else { + if (offset.left < 0) { + // component is outside the window on the left side. Move it into visible range + this.picker.addClass('datepicker-orient-left'); + left -= offset.left - visualPadding; + } else if (left + calendarWidth > windowWidth) { + // the calendar passes the widow right edge. Align it to component right side + this.picker.addClass('datepicker-orient-right'); + left += width - calendarWidth; + } else { + if (this.o.rtl) { + // Default to right + this.picker.addClass('datepicker-orient-right'); + } else { + // Default to left + this.picker.addClass('datepicker-orient-left'); + } + } + } + + // auto y orientation is best-situation: top or bottom, no fudging, + // decision based on which shows more of the calendar + var yorient = this.o.orientation.y, + top_overflow; + if (yorient === 'auto'){ + top_overflow = -scrollTop + top - calendarHeight; + yorient = top_overflow < 0 ? 'bottom' : 'top'; + } + + this.picker.addClass('datepicker-orient-' + yorient); + if (yorient === 'top') + top -= calendarHeight + parseInt(this.picker.css('padding-top')); + else + top += height; + + if (this.o.rtl) { + var right = windowWidth - (left + width); + this.picker.css({ + top: top, + right: right, + zIndex: zIndex + }); + } else { + this.picker.css({ + top: top, + left: left, + zIndex: zIndex + }); + } + return this; + }, + + _allow_update: true, + update: function(){ + if (!this._allow_update) + return this; + + var oldDates = this.dates.copy(), + dates = [], + fromArgs = false; + if (arguments.length){ + $.each(arguments, (function(i, date){ + if (date instanceof Date) + date = this._local_to_utc(date); + dates.push(date); + }).bind(this)); + fromArgs = true; + } else { + dates = this.isInput + ? this.element.val() + : this.element.data('date') || this.inputField.val(); + if (dates && this.o.multidate) + dates = dates.split(this.o.multidateSeparator); + else + dates = [dates]; + delete this.element.data().date; + } + + dates = $.map(dates, (function(date){ + return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear); + }).bind(this)); + dates = $.grep(dates, (function(date){ + return ( + !this.dateWithinRange(date) || + !date + ); + }).bind(this), true); + this.dates.replace(dates); + + if (this.o.updateViewDate) { + if (this.dates.length) + this.viewDate = new Date(this.dates.get(-1)); + else if (this.viewDate < this.o.startDate) + this.viewDate = new Date(this.o.startDate); + else if (this.viewDate > this.o.endDate) + this.viewDate = new Date(this.o.endDate); + else + this.viewDate = this.o.defaultViewDate; + } + + if (fromArgs){ + // setting date by clicking + this.setValue(); + this.element.change(); + } + else if (this.dates.length){ + // setting date by typing + if (String(oldDates) !== String(this.dates) && fromArgs) { + this._trigger('changeDate'); + this.element.change(); + } + } + if (!this.dates.length && oldDates.length) { + this._trigger('clearDate'); + this.element.change(); + } + + this.fill(); + return this; + }, + + fillDow: function(){ + if (this.o.showWeekDays) { + var dowCnt = this.o.weekStart, + html = ''; + if (this.o.calendarWeeks){ + html += ' '; + } + while (dowCnt < this.o.weekStart + 7){ + html += ''+dates[this.o.language].daysMin[(dowCnt++)%7]+''; + } + html += ''; + this.picker.find('.datepicker-days thead').append(html); + } + }, + + fillMonths: function(){ + var localDate = this._utc_to_local(this.viewDate); + var html = ''; + var focused; + for (var i = 0; i < 12; i++){ + focused = localDate && localDate.getMonth() === i ? ' focused' : ''; + html += '' + dates[this.o.language].monthsShort[i] + ''; + } + this.picker.find('.datepicker-months td').html(html); + }, + + setRange: function(range){ + if (!range || !range.length) + delete this.range; + else + this.range = $.map(range, function(d){ + return d.valueOf(); + }); + this.fill(); + }, + + getClassNames: function(date){ + var cls = [], + year = this.viewDate.getUTCFullYear(), + month = this.viewDate.getUTCMonth(), + today = UTCToday(); + if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){ + cls.push('old'); + } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){ + cls.push('new'); + } + if (this.focusDate && date.valueOf() === this.focusDate.valueOf()) + cls.push('focused'); + // Compare internal UTC date with UTC today, not local today + if (this.o.todayHighlight && isUTCEquals(date, today)) { + cls.push('today'); + } + if (this.dates.contains(date) !== -1) + cls.push('active'); + if (!this.dateWithinRange(date)){ + cls.push('disabled'); + } + if (this.dateIsDisabled(date)){ + cls.push('disabled', 'disabled-date'); + } + if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1){ + cls.push('highlighted'); + } + + if (this.range){ + if (date > this.range[0] && date < this.range[this.range.length-1]){ + cls.push('range'); + } + if ($.inArray(date.valueOf(), this.range) !== -1){ + cls.push('selected'); + } + if (date.valueOf() === this.range[0]){ + cls.push('range-start'); + } + if (date.valueOf() === this.range[this.range.length-1]){ + cls.push('range-end'); + } + } + return cls; + }, + + _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn){ + var html = ''; + var step = factor / 10; + var view = this.picker.find(selector); + var startVal = Math.floor(year / factor) * factor; + var endVal = startVal + step * 9; + var focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step; + var selected = $.map(this.dates, function(d){ + return Math.floor(d.getUTCFullYear() / step) * step; + }); + + var classes, tooltip, before; + for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) { + classes = [cssClass]; + tooltip = null; + + if (currVal === startVal - step) { + classes.push('old'); + } else if (currVal === endVal + step) { + classes.push('new'); + } + if ($.inArray(currVal, selected) !== -1) { + classes.push('active'); + } + if (currVal < startYear || currVal > endYear) { + classes.push('disabled'); + } + if (currVal === focusedVal) { + classes.push('focused'); + } + + if (beforeFn !== $.noop) { + before = beforeFn(new Date(currVal, 0, 1)); + if (before === undefined) { + before = {}; + } else if (typeof before === 'boolean') { + before = {enabled: before}; + } else if (typeof before === 'string') { + before = {classes: before}; + } + if (before.enabled === false) { + classes.push('disabled'); + } + if (before.classes) { + classes = classes.concat(before.classes.split(/\s+/)); + } + if (before.tooltip) { + tooltip = before.tooltip; + } + } + + html += '' + currVal + ''; + } + + view.find('.datepicker-switch').text(startVal + '-' + endVal); + view.find('td').html(html); + }, + + fill: function(){ + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, + todaytxt = dates[this.o.language].today || dates['en'].today || '', + cleartxt = dates[this.o.language].clear || dates['en'].clear || '', + titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat, + todayDate = UTCToday(), + titleBtnVisible = (this.o.todayBtn === true || this.o.todayBtn === 'linked') && todayDate >= this.o.startDate && todayDate <= this.o.endDate && !this.weekOfDateIsDisabled(todayDate), + tooltip, + before; + if (isNaN(year) || isNaN(month)) + return; + this.picker.find('.datepicker-days .datepicker-switch') + .text(DPGlobal.formatDate(d, titleFormat, this.o.language)); + this.picker.find('tfoot .today') + .text(todaytxt) + .css('display', titleBtnVisible ? 'table-cell' : 'none'); + this.picker.find('tfoot .clear') + .text(cleartxt) + .css('display', this.o.clearBtn === true ? 'table-cell' : 'none'); + this.picker.find('thead .datepicker-title') + .text(this.o.title) + .css('display', typeof this.o.title === 'string' && this.o.title !== '' ? 'table-cell' : 'none'); + this.updateNavArrows(); + this.fillMonths(); + var prevMonth = UTCDate(year, month, 0), + day = prevMonth.getUTCDate(); + prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); + var nextMonth = new Date(prevMonth); + if (prevMonth.getUTCFullYear() < 100){ + nextMonth.setUTCFullYear(prevMonth.getUTCFullYear()); + } + nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); + nextMonth = nextMonth.valueOf(); + var html = []; + var weekDay, clsName; + while (prevMonth.valueOf() < nextMonth){ + weekDay = prevMonth.getUTCDay(); + if (weekDay === this.o.weekStart){ + html.push(''); + if (this.o.calendarWeeks){ + // ISO 8601: First week contains first thursday. + // ISO also states week starts on Monday, but we can be more abstract here. + var + // Start of current week: based on weekstart/current date + ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5), + // Thursday of this week + th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), + // First Thursday of year, year from thursday + yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5), + // Calendar week: ms between thursdays, div ms per day, div 7 days + calWeek = (th - yth) / 864e5 / 7 + 1; + html.push(''+ calWeek +''); + } + } + clsName = this.getClassNames(prevMonth); + clsName.push('day'); + + var content = prevMonth.getUTCDate(); + + if (this.o.beforeShowDay !== $.noop){ + before = this.o.beforeShowDay(this._utc_to_local(prevMonth)); + if (before === undefined) + before = {}; + else if (typeof before === 'boolean') + before = {enabled: before}; + else if (typeof before === 'string') + before = {classes: before}; + if (before.enabled === false) + clsName.push('disabled'); + if (before.classes) + clsName = clsName.concat(before.classes.split(/\s+/)); + if (before.tooltip) + tooltip = before.tooltip; + if (before.content) + content = before.content; + } + + //Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2) + //Fallback to unique function for older jquery versions + if (typeof $.uniqueSort === "function") { + clsName = $.uniqueSort(clsName); + } else { + clsName = $.unique(clsName); + } + + html.push('' + content + ''); + tooltip = null; + if (weekDay === this.o.weekEnd){ + html.push(''); + } + prevMonth.setUTCDate(prevMonth.getUTCDate() + 1); + } + this.picker.find('.datepicker-days tbody').html(html.join('')); + + var monthsTitle = dates[this.o.language].monthsTitle || dates['en'].monthsTitle || 'Months'; + var months = this.picker.find('.datepicker-months') + .find('.datepicker-switch') + .text(this.o.maxViewMode < 2 ? monthsTitle : year) + .end() + .find('tbody span').removeClass('active'); + + $.each(this.dates, function(i, d){ + if (d.getUTCFullYear() === year) + months.eq(d.getUTCMonth()).addClass('active'); + }); + + if (year < startYear || year > endYear){ + months.addClass('disabled'); + } + if (year === startYear){ + months.slice(0, startMonth).addClass('disabled'); + } + if (year === endYear){ + months.slice(endMonth+1).addClass('disabled'); + } + + if (this.o.beforeShowMonth !== $.noop){ + var that = this; + $.each(months, function(i, month){ + var moDate = new Date(year, i, 1); + var before = that.o.beforeShowMonth(moDate); + if (before === undefined) + before = {}; + else if (typeof before === 'boolean') + before = {enabled: before}; + else if (typeof before === 'string') + before = {classes: before}; + if (before.enabled === false && !$(month).hasClass('disabled')) + $(month).addClass('disabled'); + if (before.classes) + $(month).addClass(before.classes); + if (before.tooltip) + $(month).prop('title', before.tooltip); + }); + } + + // Generating decade/years picker + this._fill_yearsView( + '.datepicker-years', + 'year', + 10, + year, + startYear, + endYear, + this.o.beforeShowYear + ); + + // Generating century/decades picker + this._fill_yearsView( + '.datepicker-decades', + 'decade', + 100, + year, + startYear, + endYear, + this.o.beforeShowDecade + ); + + // Generating millennium/centuries picker + this._fill_yearsView( + '.datepicker-centuries', + 'century', + 1000, + year, + startYear, + endYear, + this.o.beforeShowCentury + ); + }, + + updateNavArrows: function(){ + if (!this._allow_update) + return; + + var d = new Date(this.viewDate), + year = d.getUTCFullYear(), + month = d.getUTCMonth(), + startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, + startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, + endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, + endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, + prevIsDisabled, + nextIsDisabled, + factor = 1; + switch (this.viewMode){ + case 4: + factor *= 10; + /* falls through */ + case 3: + factor *= 10; + /* falls through */ + case 2: + factor *= 10; + /* falls through */ + case 1: + prevIsDisabled = Math.floor(year / factor) * factor <= startYear; + nextIsDisabled = Math.floor(year / factor) * factor + factor > endYear; + break; + case 0: + prevIsDisabled = year <= startYear && month <= startMonth; + nextIsDisabled = year >= endYear && month >= endMonth; + break; + } + + this.picker.find('.prev').toggleClass('disabled', prevIsDisabled); + this.picker.find('.next').toggleClass('disabled', nextIsDisabled); + }, + + click: function(e){ + e.preventDefault(); + e.stopPropagation(); + + var target, dir, day, year, month; + target = $(e.target); + + // Clicked on the switch + if (target.hasClass('datepicker-switch') && this.viewMode !== this.o.maxViewMode){ + this.setViewMode(this.viewMode + 1); + } + + // Clicked on today button + if (target.hasClass('today') && !target.hasClass('day')){ + this.setViewMode(0); + this._setDate(UTCToday(), this.o.todayBtn === 'linked' ? null : 'view'); + } + + // Clicked on clear button + if (target.hasClass('clear')){ + this.clearDates(); + } + + if (!target.hasClass('disabled')){ + // Clicked on a month, year, decade, century + if (target.hasClass('month') + || target.hasClass('year') + || target.hasClass('decade') + || target.hasClass('century')) { + this.viewDate.setUTCDate(1); + + day = 1; + if (this.viewMode === 1){ + month = target.parent().find('span').index(target); + year = this.viewDate.getUTCFullYear(); + this.viewDate.setUTCMonth(month); + } else { + month = 0; + year = Number(target.text()); + this.viewDate.setUTCFullYear(year); + } + + this._trigger(DPGlobal.viewModes[this.viewMode - 1].e, this.viewDate); + + if (this.viewMode === this.o.minViewMode){ + this._setDate(UTCDate(year, month, day)); + } else { + this.setViewMode(this.viewMode - 1); + this.fill(); + } + } + } + + if (this.picker.is(':visible') && this._focused_from){ + this._focused_from.focus(); + } + delete this._focused_from; + }, + + dayCellClick: function(e){ + var $target = $(e.currentTarget); + var timestamp = $target.data('date'); + var date = new Date(timestamp); + + if (this.o.updateViewDate) { + if (date.getUTCFullYear() !== this.viewDate.getUTCFullYear()) { + this._trigger('changeYear', this.viewDate); + } + + if (date.getUTCMonth() !== this.viewDate.getUTCMonth()) { + this._trigger('changeMonth', this.viewDate); + } + } + this._setDate(date); + }, + + // Clicked on prev or next + navArrowsClick: function(e){ + var $target = $(e.currentTarget); + var dir = $target.hasClass('prev') ? -1 : 1; + if (this.viewMode !== 0){ + dir *= DPGlobal.viewModes[this.viewMode].navStep * 12; + } + this.viewDate = this.moveMonth(this.viewDate, dir); + this._trigger(DPGlobal.viewModes[this.viewMode].e, this.viewDate); + this.fill(); + }, + + _toggle_multidate: function(date){ + var ix = this.dates.contains(date); + if (!date){ + this.dates.clear(); + } + + if (ix !== -1){ + if (this.o.multidate === true || this.o.multidate > 1 || this.o.toggleActive){ + this.dates.remove(ix); + } + } else if (this.o.multidate === false) { + this.dates.clear(); + this.dates.push(date); + } + else { + this.dates.push(date); + } + + if (typeof this.o.multidate === 'number') + while (this.dates.length > this.o.multidate) + this.dates.remove(0); + }, + + _setDate: function(date, which){ + if (!which || which === 'date') + this._toggle_multidate(date && new Date(date)); + if ((!which && this.o.updateViewDate) || which === 'view') + this.viewDate = date && new Date(date); + + this.fill(); + this.setValue(); + if (!which || which !== 'view') { + this._trigger('changeDate'); + } + this.inputField.trigger('change'); + if (this.o.autoclose && (!which || which === 'date')){ + this.hide(); + } + }, + + moveDay: function(date, dir){ + var newDate = new Date(date); + newDate.setUTCDate(date.getUTCDate() + dir); + + return newDate; + }, + + moveWeek: function(date, dir){ + return this.moveDay(date, dir * 7); + }, + + moveMonth: function(date, dir){ + if (!isValidDate(date)) + return this.o.defaultViewDate; + if (!dir) + return date; + var new_date = new Date(date.valueOf()), + day = new_date.getUTCDate(), + month = new_date.getUTCMonth(), + mag = Math.abs(dir), + new_month, test; + dir = dir > 0 ? 1 : -1; + if (mag === 1){ + test = dir === -1 + // If going back one month, make sure month is not current month + // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) + ? function(){ + return new_date.getUTCMonth() === month; + } + // If going forward one month, make sure month is as expected + // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) + : function(){ + return new_date.getUTCMonth() !== new_month; + }; + new_month = month + dir; + new_date.setUTCMonth(new_month); + // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 + new_month = (new_month + 12) % 12; + } + else { + // For magnitudes >1, move one month at a time... + for (var i=0; i < mag; i++) + // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)... + new_date = this.moveMonth(new_date, dir); + // ...then reset the day, keeping it in the new month + new_month = new_date.getUTCMonth(); + new_date.setUTCDate(day); + test = function(){ + return new_month !== new_date.getUTCMonth(); + }; + } + // Common date-resetting loop -- if date is beyond end of month, make it + // end of month + while (test()){ + new_date.setUTCDate(--day); + new_date.setUTCMonth(new_month); + } + return new_date; + }, + + moveYear: function(date, dir){ + return this.moveMonth(date, dir*12); + }, + + moveAvailableDate: function(date, dir, fn){ + do { + date = this[fn](date, dir); + + if (!this.dateWithinRange(date)) + return false; + + fn = 'moveDay'; + } + while (this.dateIsDisabled(date)); + + return date; + }, + + weekOfDateIsDisabled: function(date){ + return $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1; + }, + + dateIsDisabled: function(date){ + return ( + this.weekOfDateIsDisabled(date) || + $.grep(this.o.datesDisabled, function(d){ + return isUTCEquals(date, d); + }).length > 0 + ); + }, + + dateWithinRange: function(date){ + return date >= this.o.startDate && date <= this.o.endDate; + }, + + keydown: function(e){ + if (!this.picker.is(':visible')){ + if (e.keyCode === 40 || e.keyCode === 27) { // allow down to re-show picker + this.show(); + e.stopPropagation(); + } + return; + } + var dateChanged = false, + dir, newViewDate, + focusDate = this.focusDate || this.viewDate; + switch (e.keyCode){ + case 27: // escape + if (this.focusDate){ + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.fill(); + } + else + this.hide(); + e.preventDefault(); + e.stopPropagation(); + break; + case 37: // left + case 38: // up + case 39: // right + case 40: // down + if (!this.o.keyboardNavigation || this.o.daysOfWeekDisabled.length === 7) + break; + dir = e.keyCode === 37 || e.keyCode === 38 ? -1 : 1; + if (this.viewMode === 0) { + if (e.ctrlKey){ + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); + + if (newViewDate) + this._trigger('changeYear', this.viewDate); + } else if (e.shiftKey){ + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); + + if (newViewDate) + this._trigger('changeMonth', this.viewDate); + } else if (e.keyCode === 37 || e.keyCode === 39){ + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveDay'); + } else if (!this.weekOfDateIsDisabled(focusDate)){ + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveWeek'); + } + } else if (this.viewMode === 1) { + if (e.keyCode === 38 || e.keyCode === 40) { + dir = dir * 4; + } + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); + } else if (this.viewMode === 2) { + if (e.keyCode === 38 || e.keyCode === 40) { + dir = dir * 4; + } + newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); + } + if (newViewDate){ + this.focusDate = this.viewDate = newViewDate; + this.setValue(); + this.fill(); + e.preventDefault(); + } + break; + case 13: // enter + if (!this.o.forceParse) + break; + focusDate = this.focusDate || this.dates.get(-1) || this.viewDate; + if (this.o.keyboardNavigation) { + this._toggle_multidate(focusDate); + dateChanged = true; + } + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.setValue(); + this.fill(); + if (this.picker.is(':visible')){ + e.preventDefault(); + e.stopPropagation(); + if (this.o.autoclose) + this.hide(); + } + break; + case 9: // tab + this.focusDate = null; + this.viewDate = this.dates.get(-1) || this.viewDate; + this.fill(); + this.hide(); + break; + } + if (dateChanged){ + if (this.dates.length) + this._trigger('changeDate'); + else + this._trigger('clearDate'); + this.inputField.trigger('change'); + } + }, + + setViewMode: function(viewMode){ + this.viewMode = viewMode; + this.picker + .children('div') + .hide() + .filter('.datepicker-' + DPGlobal.viewModes[this.viewMode].clsName) + .show(); + this.updateNavArrows(); + this._trigger('changeViewMode', new Date(this.viewDate)); + } + }; + + var DateRangePicker = function(element, options){ + $.data(element, 'datepicker', this); + this.element = $(element); + this.inputs = $.map(options.inputs, function(i){ + return i.jquery ? i[0] : i; + }); + delete options.inputs; + + this.keepEmptyValues = options.keepEmptyValues; + delete options.keepEmptyValues; + + this._dateUpdatedHandler = this.dateUpdated.bind(this); + datepickerPlugin.call($(this.inputs), options) + .on('changeDate', this._dateUpdatedHandler); + + this.pickers = $.map(this.inputs, function(i){ + return $.data(i, 'datepicker'); + }); + this.updateDates(); + }; + DateRangePicker.prototype = { + updateDates: function(){ + this.dates = $.map(this.pickers, function(i){ + return i.getUTCDate(); + }); + this.updateRanges(); + }, + updateRanges: function(){ + var range = $.map(this.dates, function(d){ + return d.valueOf(); + }); + $.each(this.pickers, function(i, p){ + p.setRange(range); + }); + }, + clearDates: function(){ + $.each(this.pickers, function(i, p){ + p.clearDates(); + }); + }, + dateUpdated: function(e){ + // `this.updating` is a workaround for preventing infinite recursion + // between `changeDate` triggering and `setUTCDate` calling. Until + // there is a better mechanism. + if (this.updating) + return; + this.updating = true; + + var dp = $.data(e.target, 'datepicker'); + + if (dp === undefined) { + return; + } + + var new_date = dp.getUTCDate(), + keep_empty_values = this.keepEmptyValues, + i = $.inArray(e.target, this.inputs), + j = i - 1, + k = i + 1, + l = this.inputs.length; + if (i === -1) + return; + + $.each(this.pickers, function(i, p){ + if (!p.getUTCDate() && (p === dp || !keep_empty_values)) + p.setUTCDate(new_date); + }); + + if (new_date < this.dates[j]){ + // Date being moved earlier/left + while (j >= 0 && new_date < this.dates[j] && (this.pickers[j].element.val() || "").length > 0) { + this.pickers[j--].setUTCDate(new_date); + } + } else if (new_date > this.dates[k]){ + // Date being moved later/right + while (k < l && new_date > this.dates[k] && (this.pickers[k].element.val() || "").length > 0) { + this.pickers[k++].setUTCDate(new_date); + } + } + this.updateDates(); + + delete this.updating; + }, + destroy: function(){ + $.map(this.pickers, function(p){ p.destroy(); }); + $(this.inputs).off('changeDate', this._dateUpdatedHandler); + delete this.element.data().datepicker; + }, + remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead') + }; + + function opts_from_el(el, prefix){ + // Derive options from element data-attrs + var data = $(el).data(), + out = {}, inkey, + replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'); + prefix = new RegExp('^' + prefix.toLowerCase()); + function re_lower(_,a){ + return a.toLowerCase(); + } + for (var key in data) + if (prefix.test(key)){ + inkey = key.replace(replace, re_lower); + out[inkey] = data[key]; + } + return out; + } + + function opts_from_locale(lang){ + // Derive options from locale plugins + var out = {}; + // Check if "de-DE" style date is available, if not language should + // fallback to 2 letter code eg "de" + if (!dates[lang]){ + lang = lang.split('-')[0]; + if (!dates[lang]) + return; + } + var d = dates[lang]; + $.each(locale_opts, function(i,k){ + if (k in d) + out[k] = d[k]; + }); + return out; + } + + var old = $.fn.datepicker; + var datepickerPlugin = function(option){ + var args = Array.apply(null, arguments); + args.shift(); + var internal_return; + this.each(function(){ + var $this = $(this), + data = $this.data('datepicker'), + options = typeof option === 'object' && option; + if (!data){ + var elopts = opts_from_el(this, 'date'), + // Preliminary otions + xopts = $.extend({}, defaults, elopts, options), + locopts = opts_from_locale(xopts.language), + // Options priority: js args, data-attrs, locales, defaults + opts = $.extend({}, defaults, locopts, elopts, options); + if ($this.hasClass('input-daterange') || opts.inputs){ + $.extend(opts, { + inputs: opts.inputs || $this.find('input').toArray() + }); + data = new DateRangePicker(this, opts); + } + else { + data = new Datepicker(this, opts); + } + $this.data('datepicker', data); + } + if (typeof option === 'string' && typeof data[option] === 'function'){ + internal_return = data[option].apply(data, args); + } + }); + + if ( + internal_return === undefined || + internal_return instanceof Datepicker || + internal_return instanceof DateRangePicker + ) + return this; + + if (this.length > 1) + throw new Error('Using only allowed for the collection of a single element (' + option + ' function)'); + else + return internal_return; + }; + $.fn.datepicker = datepickerPlugin; + + var defaults = $.fn.datepicker.defaults = { + assumeNearbyYear: false, + autoclose: false, + beforeShowDay: $.noop, + beforeShowMonth: $.noop, + beforeShowYear: $.noop, + beforeShowDecade: $.noop, + beforeShowCentury: $.noop, + calendarWeeks: false, + clearBtn: false, + toggleActive: false, + daysOfWeekDisabled: [], + daysOfWeekHighlighted: [], + datesDisabled: [], + endDate: Infinity, + forceParse: true, + format: 'mm/dd/yyyy', + isInline: null, + keepEmptyValues: false, + keyboardNavigation: true, + language: 'en', + minViewMode: 0, + maxViewMode: 4, + multidate: false, + multidateSeparator: ',', + orientation: "auto", + rtl: false, + startDate: -Infinity, + startView: 0, + todayBtn: false, + todayHighlight: false, + updateViewDate: true, + weekStart: 0, + disableTouchKeyboard: false, + enableOnReadonly: true, + showOnFocus: true, + zIndexOffset: 10, + container: 'body', + immediateUpdates: false, + title: '', + templates: { + leftArrow: '«', + rightArrow: '»' + }, + showWeekDays: true + }; + var locale_opts = $.fn.datepicker.locale_opts = [ + 'format', + 'rtl', + 'weekStart' + ]; + $.fn.datepicker.Constructor = Datepicker; + var dates = $.fn.datepicker.dates = { + en: { + days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], + months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + today: "Today", + clear: "Clear", + titleFormat: "MM yyyy" + } + }; + + var DPGlobal = { + viewModes: [ + { + names: ['days', 'month'], + clsName: 'days', + e: 'changeMonth' + }, + { + names: ['months', 'year'], + clsName: 'months', + e: 'changeYear', + navStep: 1 + }, + { + names: ['years', 'decade'], + clsName: 'years', + e: 'changeDecade', + navStep: 10 + }, + { + names: ['decades', 'century'], + clsName: 'decades', + e: 'changeCentury', + navStep: 100 + }, + { + names: ['centuries', 'millennium'], + clsName: 'centuries', + e: 'changeMillennium', + navStep: 1000 + } + ], + validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, + nonpunctuation: /[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g, + parseFormat: function(format){ + if (typeof format.toValue === 'function' && typeof format.toDisplay === 'function') + return format; + // IE treats \0 as a string end in inputs (truncating the value), + // so it's a bad format delimiter, anyway + var separators = format.replace(this.validParts, '\0').split('\0'), + parts = format.match(this.validParts); + if (!separators || !separators.length || !parts || parts.length === 0){ + throw new Error("Invalid date format."); + } + return {separators: separators, parts: parts}; + }, + parseDate: function(date, format, language, assumeNearby){ + if (!date) + return undefined; + if (date instanceof Date) + return date; + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); + if (format.toValue) + return format.toValue(date, format, language); + var fn_map = { + d: 'moveDay', + m: 'moveMonth', + w: 'moveWeek', + y: 'moveYear' + }, + dateAliases = { + yesterday: '-1d', + today: '+0d', + tomorrow: '+1d' + }, + parts, part, dir, i, fn; + if (date in dateAliases){ + date = dateAliases[date]; + } + if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(date)){ + parts = date.match(/([\-+]\d+)([dmwy])/gi); + date = new Date(); + for (i=0; i < parts.length; i++){ + part = parts[i].match(/([\-+]\d+)([dmwy])/i); + dir = Number(part[1]); + fn = fn_map[part[2].toLowerCase()]; + date = Datepicker.prototype[fn](date, dir); + } + return Datepicker.prototype._zero_utc_time(date); + } + + parts = date && date.match(this.nonpunctuation) || []; + + function applyNearbyYear(year, threshold){ + if (threshold === true) + threshold = 10; + + // if year is 2 digits or less, than the user most likely is trying to get a recent century + if (year < 100){ + year += 2000; + // if the new year is more than threshold years in advance, use last century + if (year > ((new Date()).getFullYear()+threshold)){ + year -= 100; + } + } + + return year; + } + + var parsed = {}, + setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], + setters_map = { + yyyy: function(d,v){ + return d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v); + }, + m: function(d,v){ + if (isNaN(d)) + return d; + v -= 1; + while (v < 0) v += 12; + v %= 12; + d.setUTCMonth(v); + while (d.getUTCMonth() !== v) + d.setUTCDate(d.getUTCDate()-1); + return d; + }, + d: function(d,v){ + return d.setUTCDate(v); + } + }, + val, filtered; + setters_map['yy'] = setters_map['yyyy']; + setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; + setters_map['dd'] = setters_map['d']; + date = UTCToday(); + var fparts = format.parts.slice(); + // Remove noop parts + if (parts.length !== fparts.length){ + fparts = $(fparts).filter(function(i,p){ + return $.inArray(p, setters_order) !== -1; + }).toArray(); + } + // Process remainder + function match_part(){ + var m = this.slice(0, parts[i].length), + p = parts[i].slice(0, m.length); + return m.toLowerCase() === p.toLowerCase(); + } + if (parts.length === fparts.length){ + var cnt; + for (i=0, cnt = fparts.length; i < cnt; i++){ + val = parseInt(parts[i], 10); + part = fparts[i]; + if (isNaN(val)){ + switch (part){ + case 'MM': + filtered = $(dates[language].months).filter(match_part); + val = $.inArray(filtered[0], dates[language].months) + 1; + break; + case 'M': + filtered = $(dates[language].monthsShort).filter(match_part); + val = $.inArray(filtered[0], dates[language].monthsShort) + 1; + break; + } + } + parsed[part] = val; + } + var _date, s; + for (i=0; i < setters_order.length; i++){ + s = setters_order[i]; + if (s in parsed && !isNaN(parsed[s])){ + _date = new Date(date); + setters_map[s](_date, parsed[s]); + if (!isNaN(_date)) + date = _date; + } + } + } + return date; + }, + formatDate: function(date, format, language){ + if (!date) + return ''; + if (typeof format === 'string') + format = DPGlobal.parseFormat(format); + if (format.toDisplay) + return format.toDisplay(date, format, language); + var val = { + d: date.getUTCDate(), + D: dates[language].daysShort[date.getUTCDay()], + DD: dates[language].days[date.getUTCDay()], + m: date.getUTCMonth() + 1, + M: dates[language].monthsShort[date.getUTCMonth()], + MM: dates[language].months[date.getUTCMonth()], + yy: date.getUTCFullYear().toString().substring(2), + yyyy: date.getUTCFullYear() + }; + val.dd = (val.d < 10 ? '0' : '') + val.d; + val.mm = (val.m < 10 ? '0' : '') + val.m; + date = []; + var seps = $.extend([], format.separators); + for (var i=0, cnt = format.parts.length; i <= cnt; i++){ + if (seps.length) + date.push(seps.shift()); + date.push(val[format.parts[i]]); + } + return date.join(''); + }, + headTemplate: ''+ + ''+ + ''+ + ''+ + ''+ + ''+defaults.templates.leftArrow+''+ + ''+ + ''+defaults.templates.rightArrow+''+ + ''+ + '', + contTemplate: '', + footTemplate: ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '' + }; + DPGlobal.template = '
'+ + '
'+ + ''+ + DPGlobal.headTemplate+ + ''+ + DPGlobal.footTemplate+ + '
'+ + '
'+ + '
'+ + ''+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '
'+ + '
'+ + '
'+ + ''+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '
'+ + '
'+ + '
'+ + ''+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '
'+ + '
'+ + '
'+ + ''+ + DPGlobal.headTemplate+ + DPGlobal.contTemplate+ + DPGlobal.footTemplate+ + '
'+ + '
'+ + '
'; + + $.fn.datepicker.DPGlobal = DPGlobal; + + + /* DATEPICKER NO CONFLICT + * =================== */ + + $.fn.datepicker.noConflict = function(){ + $.fn.datepicker = old; + return this; + }; + + /* DATEPICKER VERSION + * =================== */ + $.fn.datepicker.version = '1.10.1'; + + $.fn.datepicker.deprecated = function(msg){ + var console = window.console; + if (console && console.warn) { + console.warn('DEPRECATED: ' + msg); + } + }; + + + /* DATEPICKER DATA-API + * ================== */ + + $(document).on( + 'focus.datepicker.data-api click.datepicker.data-api', + '[data-provide="datepicker"]', + function(e){ + var $this = $(this); + if ($this.data('datepicker')) + return; + e.preventDefault(); + // component click requires us to explicitly show it + datepickerPlugin.call($this, 'show'); + } + ); + $(function(){ + datepickerPlugin.call($('[data-provide="datepicker-inline"]')); + }); + +})); diff --git a/npm/packs/bootstrap-daterangepicker/abp.resourcemapping.js b/npm/packs/bootstrap-daterangepicker/abp.resourcemapping.js index b82d1e94b3..48c3b20023 100644 --- a/npm/packs/bootstrap-daterangepicker/abp.resourcemapping.js +++ b/npm/packs/bootstrap-daterangepicker/abp.resourcemapping.js @@ -1,6 +1,6 @@ module.exports = { mappings: { "@node_modules/@abp/bootstrap-daterangepicker/src/daterangepicker.js": "@libs/bootstrap-daterangepicker/", - "@node_modules/bootstrap-daterangepicker/daterangepicker.css": "@libs/bootstrap-daterangepicker/", + "@node_modules/@abp/bootstrap-daterangepicker/src/daterangepicker.css": "@libs/bootstrap-daterangepicker/", } } \ No newline at end of file diff --git a/npm/packs/bootstrap-daterangepicker/package.json b/npm/packs/bootstrap-daterangepicker/package.json index b19e0a95f8..c752ebad32 100644 --- a/npm/packs/bootstrap-daterangepicker/package.json +++ b/npm/packs/bootstrap-daterangepicker/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "@abp/moment": "~10.7.0-rc.4", - "bootstrap-daterangepicker": "^3.1.0" + "jquery": "~4.0.0", + "moment": "^2.30.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/bootstrap-daterangepicker/src/daterangepicker.css b/npm/packs/bootstrap-daterangepicker/src/daterangepicker.css new file mode 100644 index 0000000000..a96380496e --- /dev/null +++ b/npm/packs/bootstrap-daterangepicker/src/daterangepicker.css @@ -0,0 +1,410 @@ +.daterangepicker { + position: absolute; + color: inherit; + background-color: #fff; + border-radius: 4px; + border: 1px solid #ddd; + width: 278px; + max-width: none; + padding: 0; + margin-top: 7px; + top: 100px; + left: 20px; + z-index: 3001; + display: none; + font-family: arial; + font-size: 15px; + line-height: 1em; +} + +.daterangepicker:before, .daterangepicker:after { + position: absolute; + display: inline-block; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker:before { + top: -7px; + border-right: 7px solid transparent; + border-left: 7px solid transparent; + border-bottom: 7px solid #ccc; +} + +.daterangepicker:after { + top: -6px; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; +} + +.daterangepicker.opensleft:before { + right: 9px; +} + +.daterangepicker.opensleft:after { + right: 10px; +} + +.daterangepicker.openscenter:before { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; +} + +.daterangepicker.openscenter:after { + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; +} + +.daterangepicker.opensright:before { + left: 9px; +} + +.daterangepicker.opensright:after { + left: 10px; +} + +.daterangepicker.drop-up { + margin-top: -7px; +} + +.daterangepicker.drop-up:before { + top: initial; + bottom: -7px; + border-bottom: initial; + border-top: 7px solid #ccc; +} + +.daterangepicker.drop-up:after { + top: initial; + bottom: -6px; + border-bottom: initial; + border-top: 6px solid #fff; +} + +.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar { + float: none; +} + +.daterangepicker.single .drp-selected { + display: none; +} + +.daterangepicker.show-calendar .drp-calendar { + display: block; +} + +.daterangepicker.show-calendar .drp-buttons { + display: block; +} + +.daterangepicker.auto-apply .drp-buttons { + display: none; +} + +.daterangepicker .drp-calendar { + display: none; + max-width: 270px; +} + +.daterangepicker .drp-calendar.left { + padding: 8px 0 8px 8px; +} + +.daterangepicker .drp-calendar.right { + padding: 8px; +} + +.daterangepicker .drp-calendar.single .calendar-table { + border: none; +} + +.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span { + color: #fff; + border: solid black; + border-width: 0 2px 2px 0; + border-radius: 0; + display: inline-block; + padding: 3px; +} + +.daterangepicker .calendar-table .next span { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); +} + +.daterangepicker .calendar-table .prev span { + transform: rotate(135deg); + -webkit-transform: rotate(135deg); +} + +.daterangepicker .calendar-table th, .daterangepicker .calendar-table td { + white-space: nowrap; + text-align: center; + vertical-align: middle; + min-width: 32px; + width: 32px; + height: 24px; + line-height: 24px; + font-size: 12px; + border-radius: 4px; + border: 1px solid transparent; + white-space: nowrap; + cursor: pointer; +} + +.daterangepicker .calendar-table { + border: 1px solid #fff; + border-radius: 4px; + background-color: #fff; +} + +.daterangepicker .calendar-table table { + width: 100%; + margin: 0; + border-spacing: 0; + border-collapse: collapse; +} + +.daterangepicker td.available:hover, .daterangepicker th.available:hover { + background-color: #eee; + border-color: transparent; + color: inherit; +} + +.daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; +} + +.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date { + background-color: #fff; + border-color: transparent; + color: #999; +} + +.daterangepicker td.in-range { + background-color: #ebf4f8; + border-color: transparent; + color: #000; + border-radius: 0; +} + +.daterangepicker td.start-date { + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + border-radius: 4px; +} + +.daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #357ebd; + border-color: transparent; + color: #fff; +} + +.daterangepicker th.month { + width: auto; +} + +.daterangepicker td.disabled, .daterangepicker option.disabled { + color: #999; + cursor: not-allowed; + text-decoration: line-through; +} + +.daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; +} + +.daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; +} + +.daterangepicker select.yearselect { + width: 40%; +} + +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 50px; + margin: 0 auto; + background: #eee; + border: 1px solid #eee; + padding: 2px; + outline: 0; + font-size: 12px; +} + +.daterangepicker .calendar-time { + text-align: center; + margin: 4px auto 0 auto; + line-height: 30px; + position: relative; +} + +.daterangepicker .calendar-time select.disabled { + color: #ccc; + cursor: not-allowed; +} + +.daterangepicker .drp-buttons { + clear: both; + text-align: right; + padding: 8px; + border-top: 1px solid #ddd; + display: none; + line-height: 12px; + vertical-align: middle; +} + +.daterangepicker .drp-selected { + display: inline-block; + font-size: 12px; + padding-right: 8px; +} + +.daterangepicker .drp-buttons .btn { + margin-left: 8px; + font-size: 12px; + font-weight: bold; + padding: 4px 8px; +} + +.daterangepicker.show-ranges.single.rtl .drp-calendar.left { + border-right: 1px solid #ddd; +} + +.daterangepicker.show-ranges.single.ltr .drp-calendar.left { + border-left: 1px solid #ddd; +} + +.daterangepicker.show-ranges.rtl .drp-calendar.right { + border-right: 1px solid #ddd; +} + +.daterangepicker.show-ranges.ltr .drp-calendar.left { + border-left: 1px solid #ddd; +} + +.daterangepicker .ranges { + float: none; + text-align: left; + margin: 0; +} + +.daterangepicker.show-calendar .ranges { + margin-top: 8px; +} + +.daterangepicker .ranges ul { + list-style: none; + margin: 0 auto; + padding: 0; + width: 100%; +} + +.daterangepicker .ranges li { + font-size: 12px; + padding: 8px 12px; + cursor: pointer; +} + +.daterangepicker .ranges li:hover { + background-color: #eee; +} + +.daterangepicker .ranges li.active { + background-color: #08c; + color: #fff; +} + +/* Larger Screen Styling */ +@media (min-width: 564px) { + .daterangepicker { + width: auto; + } + + .daterangepicker .ranges ul { + width: 140px; + } + + .daterangepicker.single .ranges ul { + width: 100%; + } + + .daterangepicker.single .drp-calendar.left { + clear: none; + } + + .daterangepicker.single .ranges, .daterangepicker.single .drp-calendar { + float: left; + } + + .daterangepicker { + direction: ltr; + text-align: left; + } + + .daterangepicker .drp-calendar.left { + clear: left; + margin-right: 0; + } + + .daterangepicker .drp-calendar.left .calendar-table { + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .daterangepicker .drp-calendar.right { + margin-left: 0; + } + + .daterangepicker .drp-calendar.right .calendar-table { + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .daterangepicker .drp-calendar.left .calendar-table { + padding-right: 8px; + } + + .daterangepicker .ranges, .daterangepicker .drp-calendar { + float: left; + } +} + +@media (min-width: 730px) { + .daterangepicker .ranges { + width: auto; + } + + .daterangepicker .ranges { + float: left; + } + + .daterangepicker.rtl .ranges { + float: right; + } + + .daterangepicker .drp-calendar.left { + clear: none !important; + } +} diff --git a/npm/packs/bootstrap-daterangepicker/src/daterangepicker.js b/npm/packs/bootstrap-daterangepicker/src/daterangepicker.js index 0ef46e765f..c5d366d803 100644 --- a/npm/packs/bootstrap-daterangepicker/src/daterangepicker.js +++ b/npm/packs/bootstrap-daterangepicker/src/daterangepicker.js @@ -4,6 +4,8 @@ * @copyright: Copyright (c) 2012-2019 Dan Grossman. All rights reserved. * @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php * @website: http://www.daterangepicker.com/ +* Patched copy: autoApply works together with timePicker, predefined ranges start at the beginning of the day, +* and Function.prototype.bind is used instead of the jQuery.proxy API deprecated in jQuery 4. */ // Following the UMD template https://github.com/umdjs/umd/blob/master/templates/returnExportsGlobal.js (function (root, factory) { diff --git a/npm/packs/docs/package.json b/npm/packs/docs/package.json index c6bc3fdea0..284248f4c3 100644 --- a/npm/packs/docs/package.json +++ b/npm/packs/docs/package.json @@ -7,7 +7,6 @@ "dependencies": { "@abp/anchor-js": "~10.7.0-rc.4", "@abp/clipboard": "~10.7.0-rc.4", - "@abp/malihu-custom-scrollbar-plugin": "~10.7.0-rc.4", "@abp/popper.js": "~10.7.0-rc.4", "@abp/prismjs": "~10.7.0-rc.4" }, diff --git a/npm/packs/jquery-form/abp.resourcemapping.js b/npm/packs/jquery-form/abp.resourcemapping.js deleted file mode 100644 index 4ec5565b0c..0000000000 --- a/npm/packs/jquery-form/abp.resourcemapping.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - mappings: { - "@node_modules/@abp/jquery-form/src/jquery.form.min.js": "@libs/jquery-form/" - } -} \ No newline at end of file diff --git a/npm/packs/jquery-form/package.json b/npm/packs/jquery-form/package.json deleted file mode 100644 index a851196484..0000000000 --- a/npm/packs/jquery-form/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "10.7.0-rc.4", - "name": "@abp/jquery-form", - "repository": { - "type": "git", - "url": "https://github.com/abpframework/abp.git", - "directory": "npm/packs/jquery-form" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@abp/jquery": "~10.7.0-rc.4", - "jquery-form": "^4.3.0" - }, - "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", - "homepage": "https://abp.io", - "license": "LGPL-3.0", - "keywords": [ - "aspnetcore", - "boilerplate", - "framework", - "web", - "best-practices", - "angular", - "maui", - "blazor", - "mvc", - "csharp", - "webapp" - ] -} diff --git a/npm/packs/jquery-form/src/jquery.form.min.js b/npm/packs/jquery-form/src/jquery.form.min.js deleted file mode 100644 index c794a23e13..0000000000 --- a/npm/packs/jquery-form/src/jquery.form.min.js +++ /dev/null @@ -1,22 +0,0 @@ -/*! - * jQuery Form Plugin - * version: 4.3.0 - * Requires jQuery v1.7.2 or later - * Project repository: https://github.com/jquery-form/form - - * Copyright 2017 Kevin Morris - * Copyright 2006 M. Alsup - - * Dual licensed under the LGPL-2.1+ or MIT licenses - * https://github.com/jquery-form/form#license - - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - */ -!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof module&&module.exports?module.exports=function(t,r){return void 0===r&&(r="undefined"!=typeof window?require("jquery"):require("jquery")(t)),e(r),r}:e(jQuery)}(function(e){"use strict";var t=/\r?\n/g,r={};r.fileapi=void 0!==e('').get(0).files,r.formdata=void 0!==window.FormData;var a=!!e.fn.prop;function n(t){var r=t.data;t.isDefaultPrevented()||(t.preventDefault(),e(t.target).closest("form").ajaxSubmit(r))}function i(t){var r=t.target,a=e(r);if(!a.is("[type=submit],[type=image]")){var n=a.closest("[type=submit]");if(0===n.length)return;r=n[0]}var i=r.form;if(i.clk=r,"image"===r.type)if(void 0!==t.offsetX)i.clk_x=t.offsetX,i.clk_y=t.offsetY;else if("function"==typeof e.fn.offset){var o=a.offset();i.clk_x=t.pageX-o.left,i.clk_y=t.pageY-o.top}else i.clk_x=t.pageX-r.offsetLeft,i.clk_y=t.pageY-r.offsetTop;setTimeout(function(){i.clk=i.clk_x=i.clk_y=null},100)}function o(){if(e.fn.ajaxSubmit.debug){var t="[jquery.form] "+Array.prototype.join.call(arguments,"");window.console&&window.console.log?window.console.log(t):window.opera&&window.opera.postError&&window.opera.postError(t)}}e.fn.attr2=function(){if(!a)return this.attr.apply(this,arguments);var e=this.prop.apply(this,arguments);return e&&e.jquery||"string"==typeof e?e:this.attr.apply(this,arguments)},e.fn.ajaxSubmit=function(t,n,i,s){if(!this.length)return o("ajaxSubmit: skipping submit process - no element selected"),this;var u,c,l,f,d=this;"function"==typeof t?t={success:t}:"string"==typeof t||!1===t&&arguments.length>0?(t={url:t,data:n,dataType:i},"function"==typeof s&&(t.success=s)):void 0===t&&(t={}),u=t.method||t.type||this.attr2("method"),(l=(l="string"==typeof(c=t.url||this.attr2("action"))?e.trim(c):"")||window.location.href||"")&&(l=(l.match(/^([^#]+)/)||[])[1]),f=/(MSIE|Trident)/.test(navigator.userAgent||"")&&/^https/i.test(window.location.href||"")?"javascript:false":"about:blank",t=e.extend(!0,{url:l,success:e.ajaxSettings.success,type:u||e.ajaxSettings.type,iframeSrc:f},t);var m={};if(this.trigger("form-pre-serialize",[this,t,m]),m.veto)return o("ajaxSubmit: submit vetoed via form-pre-serialize trigger"),this;if(t.beforeSerialize&&!1===t.beforeSerialize(this,t))return o("ajaxSubmit: submit aborted via beforeSerialize callback"),this;var p=t.traditional;void 0===p&&(p=e.ajaxSettings.traditional);var h,v=[],g=this.formToArray(t.semantic,v,t.filtering);if(t.data){var x=e.isFunction(t.data)?t.data(g):t.data;t.extraData=x,h=e.param(x,p)}if(t.beforeSubmit&&!1===t.beforeSubmit(g,this,t))return o("ajaxSubmit: submit aborted via beforeSubmit callback"),this;if(this.trigger("form-submit-validate",[g,this,t,m]),m.veto)return o("ajaxSubmit: submit vetoed via form-submit-validate trigger"),this;var y=e.param(g,p);h&&(y=y?y+"&"+h:h),"GET"===t.type.toUpperCase()?(t.url+=(t.url.indexOf("?")>=0?"&":"?")+y,t.data=null):t.data=y;var b=[];if(t.resetForm&&b.push(function(){d.resetForm()}),t.clearForm&&b.push(function(){d.clearForm(t.includeHidden)}),!t.dataType&&t.target){var T=t.success||function(){};b.push(function(r,a,n){var i=arguments,o=t.replaceTarget?"replaceWith":"html";"html"==o&&(r=e.parseHTML(e("
").text(r).html())),e(t.target)[o](r).each(function(){T.apply(this,i)})})}else t.success&&(e.isArray(t.success)?e.merge(b,t.success):b.push(t.success));if(t.success=function(e,r,a){for(var n=t.context||this,i=0,o=b.length;i0,k="multipart/form-data",D=d.attr("enctype")===k||d.attr("encoding")===k,A=r.fileapi&&r.formdata;o("fileAPI :"+A);var F,L=(S||D)&&!A;!1!==t.iframe&&(t.iframe||L)?t.closeKeepAlive?e.get(t.closeKeepAlive,function(){F=M(g)}):F=M(g):F=(S||D)&&A?function(r){for(var a=new FormData,n=0;n',j)).css({position:"absolute",top:"-1000px",left:"-1000px"}),m=f[0],p={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(t){var r="timeout"===t?"timeout":"aborted";o("aborting upload... "+r),this.aborted=1;try{m.contentWindow.document.execCommand&&m.contentWindow.document.execCommand("Stop")}catch(e){}f.attr("src",s.iframeSrc),p.error=r,s.error&&s.error.call(s.context,p,r,t),c&&e.event.trigger("ajaxError",[p,s,r]),s.complete&&s.complete.call(s.context,p,r)}},(c=s.global)&&0==e.active++&&e.event.trigger("ajaxStart"),c&&e.event.trigger("ajaxSend",[p,s]),s.beforeSend&&!1===s.beforeSend.call(s.context,p,s))return s.global&&e.active--,T.reject(),T;if(p.aborted)return T.reject(),T;(h=b.clk)&&(g=h.name)&&!h.disabled&&(s.extraData=s.extraData||{},s.extraData[g]=h.value,"image"===h.type&&(s.extraData[g+".x"]=b.clk_x,s.extraData[g+".y"]=b.clk_y));var S=1,k=2;function D(e){var t=null;try{e.contentWindow&&(t=e.contentWindow.document)}catch(e){o("cannot get iframe.contentWindow document: "+e)}if(t)return t;try{t=e.contentDocument?e.contentDocument:e.document}catch(r){o("cannot get iframe.contentDocument: "+r),t=e.document}return t}var A=e("meta[name=csrf-token]").attr("content"),F=e("meta[name=csrf-param]").attr("content");function L(){var t=d.attr2("target"),r=d.attr2("action"),a=d.attr("enctype")||d.attr("encoding")||"multipart/form-data";b.setAttribute("target",l),u&&!/post/i.test(u)||b.setAttribute("method","POST"),r!==s.url&&b.setAttribute("action",s.url),s.skipEncodingOverride||u&&!/post/i.test(u)||d.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"}),s.timeout&&(y=setTimeout(function(){x=!0,C(S)},s.timeout));var n=[];try{if(s.extraData)for(var i in s.extraData)s.extraData.hasOwnProperty(i)&&(e.isPlainObject(s.extraData[i])&&s.extraData[i].hasOwnProperty("name")&&s.extraData[i].hasOwnProperty("value")?n.push(e('',j).val(s.extraData[i].value).appendTo(b)[0]):n.push(e('',j).val(s.extraData[i]).appendTo(b)[0]));s.iframeTarget||f.appendTo(w),m.attachEvent?m.attachEvent("onload",C):m.addEventListener("load",C,!1),setTimeout(function e(){try{var t=D(m).readyState;o("state = "+t),t&&"uninitialized"===t.toLowerCase()&&setTimeout(e,50)}catch(e){o("Server abort: ",e," (",e.name,")"),C(k),y&&clearTimeout(y),y=void 0}},15);try{b.submit()}catch(e){document.createElement("form").submit.apply(b)}}finally{b.setAttribute("action",r),b.setAttribute("enctype",a),t?b.setAttribute("target",t):d.removeAttr("target"),e(n).remove()}}F&&A&&(s.extraData=s.extraData||{},s.extraData[F]=A),s.forceSync?L():setTimeout(L,10);var E,M,O,X=50;function C(t){if(!p.aborted&&!O){if((M=D(m))||(o("cannot access response document"),t=k),t===S&&p)return p.abort("timeout"),void T.reject(p,"timeout");if(t===k&&p)return p.abort("server abort"),void T.reject(p,"error","server abort");if(M&&M.location.href!==s.iframeSrc||x){m.detachEvent?m.detachEvent("onload",C):m.removeEventListener("load",C,!1);var r,a="success";try{if(x)throw"timeout";var n="xml"===s.dataType||M.XMLDocument||e.isXMLDoc(M);if(o("isXml="+n),!n&&window.opera&&(null===M.body||!M.body.innerHTML)&&--X)return o("requeing onLoad callback, DOM not available"),void setTimeout(C,250);var i=M.body?M.body:M.documentElement;p.responseText=i?i.innerHTML:null,p.responseXML=M.XMLDocument?M.XMLDocument:M,n&&(s.dataType="xml"),p.getResponseHeader=function(e){return{"content-type":s.dataType}[e.toLowerCase()]},i&&(p.status=Number(i.getAttribute("status"))||p.status,p.statusText=i.getAttribute("statusText")||p.statusText);var u=(s.dataType||"").toLowerCase(),l=/(json|script|text)/.test(u);if(l||s.textarea){var d=M.getElementsByTagName("textarea")[0];if(d)p.responseText=d.value,p.status=Number(d.getAttribute("status"))||p.status,p.statusText=d.getAttribute("statusText")||p.statusText;else if(l){var h=M.getElementsByTagName("pre")[0],v=M.getElementsByTagName("body")[0];h?p.responseText=h.textContent?h.textContent:h.innerText:v&&(p.responseText=v.textContent?v.textContent:v.innerText)}}else"xml"===u&&!p.responseXML&&p.responseText&&(p.responseXML=q(p.responseText));try{E=_(p,u,s)}catch(e){a="parsererror",p.error=r=e||a}}catch(e){o("error caught: ",e),a="error",p.error=r=e||a}p.aborted&&(o("upload aborted"),a=null),p.status&&(a=p.status>=200&&p.status<300||304===p.status?"success":"error"),"success"===a?(s.success&&s.success.call(s.context,E,"success",p),T.resolve(p.responseText,"success",p),c&&e.event.trigger("ajaxSuccess",[p,s])):a&&(void 0===r&&(r=p.statusText),s.error&&s.error.call(s.context,p,a,r),T.reject(p,"error",r),c&&e.event.trigger("ajaxError",[p,s,r])),c&&e.event.trigger("ajaxComplete",[p,s]),c&&!--e.active&&e.event.trigger("ajaxStop"),s.complete&&s.complete.call(s.context,p,a),O=!0,s.timeout&&clearTimeout(y),setTimeout(function(){s.iframeTarget?f.attr("src",s.iframeSrc):f.remove(),p.responseXML=null},100)}}}var q=e.parseXML||function(e,t){return window.ActiveXObject?((t=new ActiveXObject("Microsoft.XMLDOM")).async="false",t.loadXML(e)):t=(new DOMParser).parseFromString(e,"text/xml"),t&&t.documentElement&&"parsererror"!==t.documentElement.nodeName?t:null},N=e.parseJSON||function(e){return window.console.error("jquery.parseJSON is undefined"),null},_=function(t,r,a){var n=t.getResponseHeader("content-type")||"",i=("xml"===r||!r)&&n.indexOf("xml")>=0,o=i?t.responseXML:t.responseText;return i&&"parsererror"===o.documentElement.nodeName&&e.error&&e.error("parsererror"),a&&a.dataFilter&&(o=a.dataFilter(o,r)),"string"==typeof o&&(("json"===r||!r)&&n.indexOf("json")>=0?o=N(o):("script"===r||!r)&&n.indexOf("javascript")>=0&&e.globalEval(o)),o};return T}},e.fn.ajaxForm=function(t,r,a,s){if(("string"==typeof t||!1===t&&arguments.length>0)&&(t={url:t,data:r,dataType:a},"function"==typeof s&&(t.success=s)),(t=t||{}).delegation=t.delegation&&e.isFunction(e.fn.on),!t.delegation&&0===this.length){var u={s:this.selector,c:this.context};return!e.isReady&&u.s?(o("DOM not ready, queuing ajaxForm"),e(function(){e(u.s,u.c).ajaxForm(t)}),this):(o("terminating; zero elements found by selector"+(e.isReady?"":" (DOM not ready)")),this)}return t.delegation?(e(document).off("submit.form-plugin",this.selector,n).off("click.form-plugin",this.selector,i).on("submit.form-plugin",this.selector,t,n).on("click.form-plugin",this.selector,t,i),this):(t.beforeFormUnbind&&t.beforeFormUnbind(this,t),this.ajaxFormUnbind().on("submit.form-plugin",t,n).on("click.form-plugin",t,i))},e.fn.ajaxFormUnbind=function(){return this.off("submit.form-plugin click.form-plugin")},e.fn.formToArray=function(t,a,n){var i=[];if(0===this.length)return i;var o,s,u,c,l,f,d,m,p=this[0],h=this.attr("id"),v=t||void 0===p.elements?p.getElementsByTagName("*"):p.elements;if(v&&(v=e.makeArray(v)),h&&(t||/(Edge|Trident)\//.test(navigator.userAgent))&&(o=e(':input[form="'+h+'"]').get()).length&&(v=(v||[]).concat(o)),!v||!v.length)return i;for(e.isFunction(n)&&(v=e.map(v,n)),s=0,d=v.length;s?@\[\\\]^`{|}~])/g, "\\$1"); + } + + function getModelPrefix(fieldName) { + return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); + } + + function appendModelPrefix(value, prefix) { + if (value.indexOf("*.") === 0) { + value = value.replace("*.", prefix); + } + return value; + } + + function onError(error, inputElement) { // 'this' is the form element + var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), + replaceAttrValue = container.attr("data-valmsg-replace"), + replace = replaceAttrValue ? JSON.parse(replaceAttrValue) !== false : null; + + container.removeClass("field-validation-valid").addClass("field-validation-error"); + error.data("unobtrusiveContainer", container); + + if (replace) { + container.empty(); + error.removeClass("input-validation-error").appendTo(container); + } + else { + error.hide(); + } + } + + function onErrors(event, validator) { // 'this' is the form element + var container = $(this).find("[data-valmsg-summary=true]"), + list = container.find("ul"); + + if (list && list.length && validator.errorList.length) { + list.empty(); + container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); + + $.each(validator.errorList, function () { + $("
  • ").html(this.message).appendTo(list); + }); + } + } + + function onSuccess(error) { // 'this' is the form element + var container = error.data("unobtrusiveContainer"); + + if (container) { + var replaceAttrValue = container.attr("data-valmsg-replace"), + replace = replaceAttrValue ? JSON.parse(replaceAttrValue) : null; + + container.addClass("field-validation-valid").removeClass("field-validation-error"); + error.removeData("unobtrusiveContainer"); + + if (replace) { + container.empty(); + } + } + } + + function onReset(event) { // 'this' is the form element + var $form = $(this), + key = '__jquery_unobtrusive_validation_form_reset'; + if ($form.data(key)) { + return; + } + // Set a flag that indicates we're currently resetting the form. + $form.data(key, true); + try { + $form.data("validator").resetForm(); + } finally { + $form.removeData(key); + } + + $form.find(".validation-summary-errors") + .addClass("validation-summary-valid") + .removeClass("validation-summary-errors"); + $form.find(".field-validation-error") + .addClass("field-validation-valid") + .removeClass("field-validation-error") + .removeData("unobtrusiveContainer") + .find(">*") // If we were using valmsg-replace, get the underlying error + .removeData("unobtrusiveContainer"); + } + + function validationInfo(form) { + var $form = $(form), + result = $form.data(data_validation), + onResetProxy = onReset.bind(form), + defaultOptions = $jQval.unobtrusive.options || {}, + execInContext = function (name, args) { + var func = defaultOptions[name]; + func && typeof func === "function" && func.apply(form, args); + }; + + if (!result) { + result = { + options: { // options structure passed to jQuery Validate's validate() method + errorClass: defaultOptions.errorClass || "input-validation-error", + errorElement: defaultOptions.errorElement || "span", + errorPlacement: function () { + onError.apply(form, arguments); + execInContext("errorPlacement", arguments); + }, + invalidHandler: function () { + onErrors.apply(form, arguments); + execInContext("invalidHandler", arguments); + }, + messages: {}, + rules: {}, + success: function () { + onSuccess.apply(form, arguments); + execInContext("success", arguments); + } + }, + attachValidation: function () { + $form + .off("reset." + data_validation) + .on("reset." + data_validation, onResetProxy) + .validate(this.options); + }, + validate: function () { // a validation function that is called by unobtrusive Ajax + $form.validate(); + return $form.valid(); + } + }; + $form.data(data_validation, result); + } + + return result; + } + + $jQval.unobtrusive = { + adapters: [], + + parseElement: function (element, skipAttach) { + /// + /// Parses a single HTML element for unobtrusive validation attributes. + /// + /// The HTML element to be parsed. + /// [Optional] true to skip attaching the + /// validation to the form. If parsing just this single element, you should specify true. + /// If parsing several elements, you should specify false, and manually attach the validation + /// to the form when you are finished. The default is false. + var $element = $(element), + form = $element.parents("form")[0], + valInfo, rules, messages; + + if (!form) { // Cannot do client-side validation without a form + return; + } + + valInfo = validationInfo(form); + valInfo.options.rules[element.name] = rules = {}; + valInfo.options.messages[element.name] = messages = {}; + + $.each(this.adapters, function () { + var prefix = "data-val-" + this.name, + message = $element.attr(prefix), + paramValues = {}; + + if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) + prefix += "-"; + + $.each(this.params, function () { + paramValues[this] = $element.attr(prefix + this); + }); + + this.adapt({ + element: element, + form: form, + message: message, + params: paramValues, + rules: rules, + messages: messages + }); + } + }); + + $.extend(rules, { "__dummy__": true }); + + if (!skipAttach) { + valInfo.attachValidation(); + } + }, + + parse: function (selector) { + /// + /// Parses all the HTML elements in the specified selector. It looks for input elements decorated + /// with the [data-val=true] attribute value and enables validation according to the data-val-* + /// attribute values. + /// + /// Any valid jQuery selector. + + // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one + // element with data-val=true + var $selector = $(selector), + $forms = $selector.parents() + .addBack() + .filter("form") + .add($selector.find("form")) + .has("[data-val=true]"); + + $selector.find("[data-val=true]").each(function () { + $jQval.unobtrusive.parseElement(this, true); + }); + + $forms.each(function () { + var info = validationInfo(this); + if (info) { + info.attachValidation(); + } + }); + } + }; + + adapters = $jQval.unobtrusive.adapters; + + adapters.add = function (adapterName, params, fn) { + /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation. + /// The name of the adapter to be added. This matches the name used + /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). + /// [Optional] An array of parameter names (strings) that will + /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and + /// mmmm is the parameter name). + /// The function to call, which adapts the values from the HTML + /// attributes into jQuery Validate rules and/or messages. + /// + if (!fn) { // Called with no params, just a function + fn = params; + params = []; + } + this.push({ name: adapterName, params: params, adapt: fn }); + return this; + }; + + adapters.addBool = function (adapterName, ruleName) { + /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where + /// the jQuery Validate validation rule has no parameter values. + /// The name of the adapter to be added. This matches the name used + /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). + /// [Optional] The name of the jQuery Validate rule. If not provided, the value + /// of adapterName will be used instead. + /// + return this.add(adapterName, function (options) { + setValidationValues(options, ruleName || adapterName, true); + }); + }; + + adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { + /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where + /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and + /// one for min-and-max). The HTML parameters are expected to be named -min and -max. + /// The name of the adapter to be added. This matches the name used + /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name). + /// The name of the jQuery Validate rule to be used when you only + /// have a minimum value. + /// The name of the jQuery Validate rule to be used when you only + /// have a maximum value. + /// The name of the jQuery Validate rule to be used when you + /// have both a minimum and maximum value. + /// [Optional] The name of the HTML attribute that + /// contains the minimum value. The default is "min". + /// [Optional] The name of the HTML attribute that + /// contains the maximum value. The default is "max". + /// + return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { + var min = options.params.min, + max = options.params.max; + + if (min && max) { + setValidationValues(options, minMaxRuleName, [min, max]); + } + else if (min) { + setValidationValues(options, minRuleName, min); + } + else if (max) { + setValidationValues(options, maxRuleName, max); + } + }); + }; + + adapters.addSingleVal = function (adapterName, attribute, ruleName) { + /// Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where + /// the jQuery Validate validation rule has a single value. + /// The name of the adapter to be added. This matches the name used + /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name). + /// [Optional] The name of the HTML attribute that contains the value. + /// The default is "val". + /// [Optional] The name of the jQuery Validate rule. If not provided, the value + /// of adapterName will be used instead. + /// + return this.add(adapterName, [attribute || "val"], function (options) { + setValidationValues(options, ruleName || adapterName, options.params[attribute]); + }); + }; + + $jQval.addMethod("__dummy__", function (value, element, params) { + return true; + }); + + $jQval.addMethod("regex", function (value, element, params) { + var match; + if (this.optional(element)) { + return true; + } + + match = new RegExp(params).exec(value); + return (match && (match.index === 0) && (match[0].length === value.length)); + }); + + $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { + var match; + if (nonalphamin) { + match = value.match(/\W/g); + match = match && match.length >= nonalphamin; + } + return match; + }); + + if ($jQval.methods.extension) { + adapters.addSingleVal("accept", "mimtype"); + adapters.addSingleVal("extension", "extension"); + } else { + // for backward compatibility, when the 'extension' validation method does not exist, such as with versions + // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for + // validating the extension, and ignore mime-type validations as they are not supported. + adapters.addSingleVal("extension", "extension", "accept"); + } + + adapters.addSingleVal("regex", "pattern"); + adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); + adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); + adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); + adapters.add("equalto", ["other"], function (options) { + var prefix = getModelPrefix(options.element.name), + other = options.params.other, + fullOtherName = appendModelPrefix(other, prefix), + element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; + + setValidationValues(options, "equalTo", element); + }); + adapters.add("required", function (options) { + // jQuery Validate equates "required" with "mandatory" for checkbox elements + if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { + setValidationValues(options, "required", true); + } + }); + adapters.add("remote", ["url", "type", "additionalfields"], function (options) { + var value = { + url: options.params.url, + type: options.params.type || "GET", + data: {} + }, + prefix = getModelPrefix(options.element.name); + + $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { + var paramName = appendModelPrefix(fieldName, prefix); + value.data[paramName] = function () { + var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); + // For checkboxes and radio buttons, only pick up values from checked fields. + if (field.is(":checkbox")) { + return field.filter(":checked").val() || field.filter(":hidden").val() || ''; + } + else if (field.is(":radio")) { + return field.filter(":checked").val() || ''; + } + return field.val(); + }; + }); + + setValidationValues(options, "remote", value); + }); + adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { + if (options.params.min) { + setValidationValues(options, "minlength", options.params.min); + } + if (options.params.nonalphamin) { + setValidationValues(options, "nonalphamin", options.params.nonalphamin); + } + if (options.params.regex) { + setValidationValues(options, "regex", options.params.regex); + } + }); + adapters.add("fileextensions", ["extensions"], function (options) { + setValidationValues(options, "extension", options.params.extensions); + }); + + $(function () { + $jQval.unobtrusive.parse(document); + }); + + return $jQval.unobtrusive; +})); diff --git a/npm/packs/malihu-custom-scrollbar-plugin/README.md b/npm/packs/malihu-custom-scrollbar-plugin/README.md deleted file mode 100644 index 8416714abe..0000000000 --- a/npm/packs/malihu-custom-scrollbar-plugin/README.md +++ /dev/null @@ -1,129 +0,0 @@ -## ℹ️ Description - -ABP Framework is a complete open-source infrastructure to create modern web applications by following the best practices and conventions of software development. This package is a part of the [ABP Framework](https://abp.io) and contains client-side files. -For more information, check out the below links: - -🔗Official Website: https://abp.io - -🔗Commercial Website: https://commercial.abp.io - -🔗Commercial Demo: https://commercial.abp.io/demo - -🔗GitHub Repository: https://github.com/abpframework/abp - -🔗Official Theme: https://www.LeptonTheme.com - -🔗Documentation: https://docs.abp.io - -🔗Community: https://community.abp.io - -🔗Blog: https://blog.abp.io - -🔗Books: https://abp.io/books - -🔗Twitter: https://twitter.com/abpframework - -🔗Discord: https://community.abp.io/discord - -🔗Stackoverflow: https://stackoverflow.com/questions/tagged/abp - -🔗YouTube: https://www.youtube.com/@Volosoft - - -## 🤔 Why ABP Platform? - -Why should you use the ABP.IO Platform instead of creating a new solution from scratch? - -You can find the answer here 👉🏻 [Why ABP Platform?](https://docs.abp.io/en/commercial/latest/why-abp-io-platform) - -## 🚀 Key Features of the ABP Framework - -🟡 Modularity - -🟡 Multi-Tenancy - -🟡 Bootstrap Tag Helpers - -🟡 Dynamic Forms - -🟡 Authentication - -🟡 Authorization - -🟡 Distributed Event Bus - -🟡 BLOB Storing - -🟡 Text Templating - -🟡 Tooling: ABP CLI - -🟡 Cross-Cutting Concerns - -🟡 Bundling & Minification - -🟡 Virtual File System - -🟡 Theming - -🟡 Background Jobs - -🟡 DDD Infrastructure - -🟡 Auto REST APIs - -🟡 Dynamic Client Proxies - -🟡 Multiple Database Providers - -🟡 Data filtering - -🟡 Test Infrastructure - -🟡 Audit Logging - -🟡 Object to Object Mapping - -🟡 Email & SMS Abstractions - -🟡 Localization - -🟡 Setting Management - -🟡 Extension Methods - -🟡 Aspect Oriented Programming - -🟡 Dependency Injection - - -## 🧐 How It Works? - -The following page explains how you use the ABP.IO Platform as a .NET developer 👉 [How it works?](https://commercial.abp.io/how-it-works) - - -### 📘 Supported Database Providers - -🔵 Entity Framework Core - -🔵 MongoDB - -🔵 Dapper - - -### 🎴 Supported UI Frameworks - -🔵 Angular - -🔵 Razor Pages - -🔵 Blazor Web Assembly - -🔵 Blazor Server - -🔵 MAUI with Blazor Hybrid - - -## 📫 Bug & Support - -Support for open-source ABP Framework client-side packages is available at [GitHub Issues](https://github.com/abpframework/abp/issues), and the commercial support is available at [support.abp.io](https://support.abp.io). diff --git a/npm/packs/malihu-custom-scrollbar-plugin/abp.resourcemapping.js b/npm/packs/malihu-custom-scrollbar-plugin/abp.resourcemapping.js deleted file mode 100644 index 7e7f1001e6..0000000000 --- a/npm/packs/malihu-custom-scrollbar-plugin/abp.resourcemapping.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - mappings: { - "@node_modules/malihu-custom-scrollbar-plugin/*.*": "@libs/malihu-custom-scrollbar-plugin/" - } -} \ No newline at end of file diff --git a/npm/packs/malihu-custom-scrollbar-plugin/package.json b/npm/packs/malihu-custom-scrollbar-plugin/package.json deleted file mode 100644 index 9276d64b59..0000000000 --- a/npm/packs/malihu-custom-scrollbar-plugin/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "10.7.0-rc.4", - "name": "@abp/malihu-custom-scrollbar-plugin", - "repository": { - "type": "git", - "url": "https://github.com/abpframework/abp.git", - "directory": "npm/packs/malihu-custom-scrollbar-plugin" - }, - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@abp/core": "~10.7.0-rc.4", - "malihu-custom-scrollbar-plugin": "^3.1.5" - }, - "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", - "homepage": "https://abp.io", - "license": "LGPL-3.0", - "keywords": [ - "aspnetcore", - "boilerplate", - "framework", - "web", - "best-practices", - "angular", - "maui", - "blazor", - "mvc", - "csharp", - "webapp" - ] -} diff --git a/npm/packs/owl.carousel/README.md b/npm/packs/owl.carousel/README.md deleted file mode 100644 index 8416714abe..0000000000 --- a/npm/packs/owl.carousel/README.md +++ /dev/null @@ -1,129 +0,0 @@ -## ℹ️ Description - -ABP Framework is a complete open-source infrastructure to create modern web applications by following the best practices and conventions of software development. This package is a part of the [ABP Framework](https://abp.io) and contains client-side files. -For more information, check out the below links: - -🔗Official Website: https://abp.io - -🔗Commercial Website: https://commercial.abp.io - -🔗Commercial Demo: https://commercial.abp.io/demo - -🔗GitHub Repository: https://github.com/abpframework/abp - -🔗Official Theme: https://www.LeptonTheme.com - -🔗Documentation: https://docs.abp.io - -🔗Community: https://community.abp.io - -🔗Blog: https://blog.abp.io - -🔗Books: https://abp.io/books - -🔗Twitter: https://twitter.com/abpframework - -🔗Discord: https://community.abp.io/discord - -🔗Stackoverflow: https://stackoverflow.com/questions/tagged/abp - -🔗YouTube: https://www.youtube.com/@Volosoft - - -## 🤔 Why ABP Platform? - -Why should you use the ABP.IO Platform instead of creating a new solution from scratch? - -You can find the answer here 👉🏻 [Why ABP Platform?](https://docs.abp.io/en/commercial/latest/why-abp-io-platform) - -## 🚀 Key Features of the ABP Framework - -🟡 Modularity - -🟡 Multi-Tenancy - -🟡 Bootstrap Tag Helpers - -🟡 Dynamic Forms - -🟡 Authentication - -🟡 Authorization - -🟡 Distributed Event Bus - -🟡 BLOB Storing - -🟡 Text Templating - -🟡 Tooling: ABP CLI - -🟡 Cross-Cutting Concerns - -🟡 Bundling & Minification - -🟡 Virtual File System - -🟡 Theming - -🟡 Background Jobs - -🟡 DDD Infrastructure - -🟡 Auto REST APIs - -🟡 Dynamic Client Proxies - -🟡 Multiple Database Providers - -🟡 Data filtering - -🟡 Test Infrastructure - -🟡 Audit Logging - -🟡 Object to Object Mapping - -🟡 Email & SMS Abstractions - -🟡 Localization - -🟡 Setting Management - -🟡 Extension Methods - -🟡 Aspect Oriented Programming - -🟡 Dependency Injection - - -## 🧐 How It Works? - -The following page explains how you use the ABP.IO Platform as a .NET developer 👉 [How it works?](https://commercial.abp.io/how-it-works) - - -### 📘 Supported Database Providers - -🔵 Entity Framework Core - -🔵 MongoDB - -🔵 Dapper - - -### 🎴 Supported UI Frameworks - -🔵 Angular - -🔵 Razor Pages - -🔵 Blazor Web Assembly - -🔵 Blazor Server - -🔵 MAUI with Blazor Hybrid - - -## 📫 Bug & Support - -Support for open-source ABP Framework client-side packages is available at [GitHub Issues](https://github.com/abpframework/abp/issues), and the commercial support is available at [support.abp.io](https://support.abp.io). diff --git a/npm/packs/owl.carousel/abp.resourcemapping.js b/npm/packs/owl.carousel/abp.resourcemapping.js deleted file mode 100644 index 3f540f8a28..0000000000 --- a/npm/packs/owl.carousel/abp.resourcemapping.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - mappings: { - "@node_modules/owl.carousel/dist/**/*.*": "@libs/owl.carousel/" - } -} \ No newline at end of file diff --git a/npm/packs/jquery-form/README.md b/npm/packs/swiper/README.md similarity index 100% rename from npm/packs/jquery-form/README.md rename to npm/packs/swiper/README.md diff --git a/npm/packs/swiper/abp.resourcemapping.js b/npm/packs/swiper/abp.resourcemapping.js new file mode 100644 index 0000000000..9917ce0bee --- /dev/null +++ b/npm/packs/swiper/abp.resourcemapping.js @@ -0,0 +1,6 @@ +module.exports = { + mappings: { + "@node_modules/swiper/swiper-bundle.min.js*": "@libs/swiper/", + "@node_modules/swiper/swiper-bundle.min.css": "@libs/swiper/" + } +} diff --git a/npm/packs/owl.carousel/package.json b/npm/packs/swiper/package.json similarity index 84% rename from npm/packs/owl.carousel/package.json rename to npm/packs/swiper/package.json index fe6699e404..780ab22e79 100644 --- a/npm/packs/owl.carousel/package.json +++ b/npm/packs/swiper/package.json @@ -1,12 +1,11 @@ { "version": "10.7.0-rc.4", - "name": "@abp/owl.carousel", + "name": "@abp/swiper", "publishConfig": { "access": "public" }, "dependencies": { - "@abp/core": "~10.7.0-rc.4", - "owl.carousel": "^2.3.4" + "swiper": "^14.1.0" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/timeago/abp.resourcemapping.js b/npm/packs/timeago/abp.resourcemapping.js index df913e3d28..de0c3464db 100644 --- a/npm/packs/timeago/abp.resourcemapping.js +++ b/npm/packs/timeago/abp.resourcemapping.js @@ -1,6 +1,6 @@ module.exports = { mappings: { - "@node_modules/timeago/jquery.timeago.js": "@libs/timeago/", - "@node_modules/timeago/locales/*.*": "@libs/timeago/locales/" + "@node_modules/timeago.js/dist/*.js": "@libs/timeago/", + "@node_modules/@abp/timeago/src/*.*": "@libs/abp/timeago/" } -} \ No newline at end of file +} diff --git a/npm/packs/timeago/package.json b/npm/packs/timeago/package.json index 9e86908a55..4c318874f7 100644 --- a/npm/packs/timeago/package.json +++ b/npm/packs/timeago/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@abp/jquery": "~10.7.0-rc.4", - "timeago": "^1.6.7" + "timeago.js": "^4.0.2" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", diff --git a/npm/packs/timeago/src/abp.timeago.js b/npm/packs/timeago/src/abp.timeago.js new file mode 100644 index 0000000000..40693779a7 --- /dev/null +++ b/npm/packs/timeago/src/abp.timeago.js @@ -0,0 +1,164 @@ +var abp = abp || {}; +(function ($) { + + if (typeof timeago === 'undefined') { + throw "abp/timeago library requires the timeago.js library included to the page!"; + } + + abp.timeago = abp.timeago || {}; + + // ABP culture name -> timeago.js locale name (only the ones that don't match the two-letter language code) + abp.timeago.localeMap = { + 'en': 'en_US', + 'zh': 'zh_CN', + 'zh-Hans': 'zh_CN', + 'zh-Hant': 'zh_TW', + 'zh-CN': 'zh_CN', + 'zh-TW': 'zh_TW', + 'pt': 'pt_BR', + 'nb': 'nb_NO', + 'no': 'nb_NO', + 'nn': 'nn_NO', + 'hi': 'hi_IN', + 'id': 'id_ID', + 'bn': 'bn_IN' + }; + + abp.timeago.getLocale = function (cultureName) { + cultureName = cultureName || (abp.localization && abp.localization.currentCulture && abp.localization.currentCulture.cultureName) || 'en'; + + if (abp.timeago.localeMap[cultureName]) { + return abp.timeago.localeMap[cultureName]; + } + + var language = cultureName.split('-')[0]; + return abp.timeago.localeMap[language] || language; + }; + + abp.timeago.format = function (date, options) { + return timeago.format(date, abp.timeago.getLocale(), options); + }; + + var toNodeList = function (nodes) { + if (!nodes) { + return []; + } + + return Array.prototype.filter.call(nodes.nodeType ? [nodes] : nodes, function (node) { + return node && node.getAttribute; + }); + }; + + //