diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 18c5524d1f..1dc0d7c1f7 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -382,6 +382,17 @@ "SuccessfullyAdded": "Successfully added", "PurchaseState": "Purchase State", "ShowBetweenDayCount": "Show Between Days", - "PurchaseOrder": "Purchase Order" + "PurchaseOrder": "Purchase Order", + "ShowCreateInvoiceOfOrganization": "Create Invoice", + "ShowCreateQuotationOfOrganization": "Create Quotation", + "BookDiscounts": "Book Discounts", + "Permission:BookDiscount": "Book Discount", + "Menu:BookDiscounts": "Book Discounts", + "BookType": "Book Type", + "PurchasePlatform": "Purchase Platform", + "StartTime": "Start Time", + "EndTime": "End Time", + "CreateABookDiscount": "Create a book discount", + "BookDiscountDeletionConfirmationMessage": "Are you sure you want to delete this book discount?" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index 5d6e081085..e898619b64 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -121,6 +121,10 @@ "MasteringAbpFrameworkBook": "Book: Mastering ABP Framework", "ABPIO-CommonPreferenceDefinition": "Get the latest news about ABP Platform like new posts, events and more.", "BuiltOn": "Built-on", - "AbpFramework": "ABP Framework" + "AbpFramework": "ABP Framework", + "Volo.AbpIo.Domain:080001": "Start Time can not be greater than End Time", + "Enum:BookType:0": "Mastering ABP Framework", + "Enum:PurchasePlatform:0": "Amazon", + "Enum:PurchasePlatform:1": "Packt" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 0d7724df56..5a63ea7869 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -49,7 +49,7 @@ "IndexPageHeroSection": "A complete web development platformbuilt-on framework", "AbpCommercialShortDescription": "ABP Commercial provides pre-built application modules, rapid application development tooling, professional UI themes, premium support and more.", "LiveDemo": "Live Demo", - "GetLicence": "Get a Licence", + "GetLicence": "Get a License", "Application": "Application", "StartupTemplates": "Startup Templates", "Startup": "Startup", @@ -199,12 +199,13 @@ "WhatHappensWhenLicenseEndsExplanation5": "You can not use the ABP Suite.", "WhatHappensWhenLicenseEndsExplanation6": "You can not get the premium support anymore.", "WhatHappensWhenLicenseEndsExplanation7": "You can extend (renew) your license if you want to continue getting these benefits. If you extend your license within 1 month after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount.", + "WhatHappensWhenLicenseEndsExplanation8": "The ABP projects you generated are not stored on our servers. Therefore, it is your responsibility to keep the source code you download. When your license expires, there's no way to get your generated ABP project source code.", "WhenShouldIRenewMyLicense": "When should I renew my license?", "WhenShouldIRenewMyLicenseExplanation": "If you renew your license within 1 month after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount. However, if you renew your license after 1 month since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.", "TrialPlan": "Do you have a trial plan?", "TrialPlanExplanation": "For now, ABP Commercial doesn't have a trial plan. For the Team licenses we provide 30 days money back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.", "DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?", - "DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.
After sending the license fee via bank transfer, email us your receipt and the type of license requested at accounting@abp.io, here's our international bank account information:", + "DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.
After sending the license fee via bank transfer, send your receipt and requested license type to accounting@abp.io.
Our international bank account information:", "HowToUpgrade": "How to upgrade existing applications when a new version is available?", "HowToUpgradeExplanation1": "When you create a new application using ABP Commercial, all the modules and theme are used as NuGet and NPM packages. So, you can easily upgrade the packages when a new version is available.", "HowToUpgradeExplanation2": "In addition to the standard NuGet/NPM upgrades, ABP CLI provides an update command that automatically finds and upgrades all ABP related packages in your solution.", @@ -271,7 +272,7 @@ "Enterprise": "Enterprise", "Custom": "Custom", "IncludedDeveloperLicenses": "Included developer licenses", - "CustomLicenceOrAdditionalServices": "Need custom licence or additional services?", + "CustomLicenceOrAdditionalServices": "Need custom license or additional services?", "CustomOrVolumeLicense": "Custom or volume license", "LiveTrainingSupport": "Live training & support", "AndMore": "and more", @@ -506,6 +507,7 @@ "OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?", "OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at info@abp.io.", "AddBasket": "Add to Basket", - "SendTrainingRequest": "Send Training Request" + "SendTrainingRequest": "Send Training Request", + "OnlyEnglishVersionOfThisDocumentIsTheRecentAndValid": "* The English version of this document is the most up-to-date and the English version will prevail in any dispute." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 0b9d6d96b2..35b755e8c3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -214,7 +214,11 @@ "SeeDocs": "See Docs", "None": "None", "Application": "Application", + "ApplicationExplanation": "Creates a fully layered solution based on Domain Driven Design practices. Recommended for long-term projects that need a maintainable and extensible codebase.", + "ApplicationNoLayer": "Application (single layer)", + "ApplicationNoLayerExplanation": "Creates a single-layer web application. Recommended for building an application with a simpler and easy to understand architecture.", "Module": "Module", + "ModuleExplanation": "Creates a reusable, fully layered application module solution. You can use this option to create modules for your modular application.", "PackageName": "Package Name", "LicenseURL": "License URL", "License": "License", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json index f9c6d54204..ad8393fab3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json @@ -214,7 +214,11 @@ "SeeDocs": "Dökümanı Görüntüle", "None": "Hiç", "Application": "Uygulama", + "ApplicationExplanation": "Domain Driven Design pratikleri üzerine oluşturulmuş çok katmanlı bir çözüm oluşturur. Bakıma ve geliştirmeye açık kod tabanına ihtiyaç duyan uzun süreli projeler için önerilir.", + "ApplicationNoLayer": "Uygulama (tek katmanlı)", + "ApplicationNoLayerExplanation": "Tek katmanlı Web uygulaması oluşturur. Daha basit ve anlaması kolay mimari ile uygulama geliştirmek için önerilir.", "Module": "Modül", + "ModuleExplanation": "Tamamen katmanlanmış, tekrar kullanılabilir bir uygulama modülü oluşturur. Uygulamanıza modül yaratmak için bu seçeneği kullanabilirsiniz.", "PackageName": "Paket adı", "LicenseURL": "Lisans Linki", "License": "Lisans", diff --git a/docs/en/Autofac-Integration.md b/docs/en/Autofac-Integration.md index 1313d2c482..d05412c94b 100644 --- a/docs/en/Autofac-Integration.md +++ b/docs/en/Autofac-Integration.md @@ -1,12 +1,12 @@ # Autofac Integration -Autofac is one of the most used dependency injection frameworks for .Net. It provides some advanced features compared to .Net Core standard DI library, like dynamic proxying and property injection. +[Autofac](https://autofac.org/) is one of the most used dependency injection frameworks for .NET. It provides advanced features compared to .Net Core's standard DI library, like dynamic proxying and property injection. ## Install Autofac Integration -> All startup templates and samples are Autofac integrated. So, most of the time you don't need to manually install this package. +> All the [startup templates](Startup-Templates/Index.md) and samples are Autofac integrated. So, most of the time you don't need to manually install this package. -Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to your project (for a multi-projects application, it's suggested to add to the executable/web project.) +Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to your project (for a multi-projects application, it's suggested to add to the executable/web project). ```` Install-Package Volo.Abp.Autofac @@ -52,6 +52,23 @@ public class Program } ```` +If you are using the static `WebApplication` class, you can call the `UseAutofac()` extension method as shown below: + +````csharp +public class Program +{ + public async static Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseAutofac(); // Integrate Autofac! + await builder.AddApplicationAsync(); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); + } +} +```` + ### Console Application Call `UseAutofac()` method in the `AbpApplicationFactory.Create` options as shown below: @@ -79,3 +96,18 @@ namespace AbpConsoleDemo } ```` +## Using the Autofac Registration API + +If you want to use Autofac's advanced [registration API](https://autofac.readthedocs.io/en/latest/register/registration.html), you need to access the `ContainerBuilder` object. [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package defines the `IServiceCollection.GetContainerBuilder()` extension method to obtain the `ContainerBuilder` object. + +**Example: Get the `ContainerBuilder` object in the `ConfigureServices` method of your [module class](Module-Development-Basics.md)** + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var containerBuilder = context.Services.GetContainerBuilder(); + containerBuilder.RegisterType(); // Using Autofac's registration API +} +```` + +> You should install the [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to the project that you want to use the Autofac API. diff --git a/docs/en/Background-Jobs.md b/docs/en/Background-Jobs.md index 2e0b5d3f65..84e746f73d 100644 --- a/docs/en/Background-Jobs.md +++ b/docs/en/Background-Jobs.md @@ -155,8 +155,6 @@ public class MyModule : AbpModule } ```` -> Default background manager (see below) does not support multiple processes execute the same job queue. So, if you have multiple running instance of your application and you are using the default background job manager, you should only enable one application instance process the job queue. - ## Default Background Job Manager ABP framework includes a simple `IBackgroundJobManager` implementation that; @@ -189,12 +187,23 @@ public class MyModule : AbpModule ### Data Store -The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction. So, you can replace the implementation if you want. +The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction to store the jobs. -Background Jobs module implements `IBackgroundJobStore` using various data access providers. See its own [documentation](Modules/Background-Jobs.md). +Background Jobs module implements `IBackgroundJobStore` using various data access providers. See its own [documentation](Modules/Background-Jobs.md). If you don't want to use this module, you should implement the `IBackgroundJobStore` interface yourself. > Background Jobs module is already installed to the startup templates by default and it works based on your ORM/data access choice. +### Clustered Deployment + +The default background job manager is compatible with [clustered environments](Deployment/Clustered-Environment.md) (where multiple instances of your application run concurrently). It uses a [distributed lock](Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time. + +However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, **please follow the [distributed lock](Distributed-Locking.md) document to configure a provider for your application**, if it is not already configured. + +If you don't want to use a distributed lock provider, you may go with the following options: + +* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs). +* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance. + ## Integrations Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations. diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md index 726a8817ab..e1cce6ae28 100644 --- a/docs/en/Background-Workers.md +++ b/docs/en/Background-Workers.md @@ -126,10 +126,11 @@ Background workers only work if your application is running. If you host the bac Be careful if you run multiple instances of your application simultaneously in a clustered environment. In that case, every application runs the same worker which may create conflicts if your workers are running on the same resources (processing the same data, for example). -If that's a problem for your workers, you have two options; +If that's a problem for your workers, you have the following options: -* Disable the background worker system using the `AbpBackgroundWorkerOptions` described above, for all the application instances, except one of them. -* Disable the background worker system for all the application instances and create another special application that runs on a single server and execute the workers. +* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](../Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed. +* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers. +* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance. ## Integrations diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 72fe0cf221..8931220705 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -299,7 +299,7 @@ abp generate-proxy -t [options] Examples: ````bash -abp generate-proxy -t ng +abp generate-proxy -t ng -url https://localhost:44302/ abp generate-proxy -t js -url https://localhost:44302/ abp generate-proxy -t csharp -url https://localhost:44302/ ```` @@ -313,12 +313,13 @@ abp generate-proxy -t csharp -url https://localhost:44302/ * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. + * `--url`: Specifies api definition url. Default value is API Name's url in environment file. * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: * `--output` or `-o`: JavaScript file path or folder to place generated code in. * `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. * `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. For `csharp` and `js` client types. +* `--url` or `-u`: API definition URL from. > See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more. @@ -351,12 +352,13 @@ abp remove-proxy -t csharp --folder MyProxies/InnerFolder * `--api-name` or `-a`: The name of the API endpoint defined in the `/src/environments/environment.ts`. Default value: `default`. * `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`. * `--target`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`. + * `--url`: Specifies api definition url. Default value is API Name's url in environment file. * `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options). * `js`: JavaScript. work in the `*.Web` project directory. There are some additional options for this client: * `--output` or `-o`: JavaScript file path or folder to place generated code in. * `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`. * `--working-directory` or `-wd`: Execution directory. For `csharp` and `js` client types. -* `--url` or `-u`: API definition URL from. For `csharp` and `js` client types. +* `--url` or `-u`: API definition URL from. > See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more. diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 074d6d951f..8fafd725be 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -1,7 +1,9 @@ -# Caching +# Distributed Caching ABP Framework extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). +> **Default implementation of the `IDistributedCache` interface is` MemoryDistributedCache` which works in-memory.** See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use Redis as the distributed cache server. + ## Installation > This package is already installed by default with the [application startup template](Startup-Templates/Application.md). So, most of the time, you don't need to install it manually. @@ -27,8 +29,6 @@ ASP.NET Core defines the `IDistributedCache` interface to get/set the cache valu > `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications, but also available to **any type of applications**. -> Default implementation of the `IDistributedCache` interface is the `MemoryDistributedCache` which works **in-memory**. See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](Redis-Cache.md) document if you want to use the Redis as the distributed cache server. - See [ASP.NET Core's distributed caching document](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more information. ### `IDistributedCache` Interface diff --git a/docs/en/Deployment/Clustered-Environment.md b/docs/en/Deployment/Clustered-Environment.md new file mode 100644 index 0000000000..d76a199518 --- /dev/null +++ b/docs/en/Deployment/Clustered-Environment.md @@ -0,0 +1,99 @@ +# Deploying to a Clustered Environment + +This document introduces the topics that you should consider when you are deploying your application to a clustered environment where **multiple instances of your application run concurrently**, and explains how you can deal with these topics in your ABP based application. + +> This document is valid regardless you have a monolith application or a microservice solution. The Application term is used for a process. An application can be a monolith web application, a service in a microservice solution, a console application, or another kind of an executable process. +> +> For example, if you are deploying your application to Kubernetes and configure your application or service to run in multiple pods, then your application or service runs in a clustered environment. + +## Understanding the Clustered Environment + +> You can skip this section if you are already familiar with clustered deployment and load balancers. + +### Single Instance Deployment + +Consider an application deployed as a **single instance**, as illustrated in the following figure: + +![deployment-single-instance](../images/deployment-single-instance.png) + +Browsers and other client applications can directly make HTTP requests to your application. You can put a web server (e.g. IIS or NGINX) between the clients and your application, but you still have a single application instance running in a single server or container. Single-instance configuration is **limited to scale** since it runs in a single server and you are limited with the server's capacity. + +### Clustered Deployment + +**Clustered deployment** is the way of running **multiple instances** of your application **concurrently** in a single or multiple servers. In this way, different instances can serve different requests and you can scale by adding new servers to the system. The following figure shows a typical implementation of clustering using a **load balancer**: + +![deployment-clustered](../images/deployment-clustered.png) + +### Load Balancers + +[Load balancers](https://en.wikipedia.org/wiki/Load_balancing_(computing)) have a lot of features, but they fundamentally **forward an incoming HTTP request** to an instance of your application and return your response back to the client application. + +Load balancers can use different algorithms for selecting the application instance while determining the application instance that is used to deliver the incoming request. **Round Robin** is one of the simplest and most used algorithms. Requests are delivered to the application instances in rotation. First instance gets the first request, second instance gets the second, and so on. It returns to the first instance after all the instances are used, and the algorithm goes like that for the next requests. + +### Potential Problems + +Once multiple instances of your application run in parallel, you should carefully consider the following topics: + +* Any **state (data) stored in memory** of your application will become a problem when you have multiple instances. A state stored in memory of an application instance may not be available in the next request since the next request will be handled by a different application instance. While there are some solutions (like sticky sessions) to overcome this problem user-basis, it is a **best practice to design your application as stateless** if you want to run it in a cluster, container or/and cloud. +* **In-memory caching** is a kind of in-memory state and should not be used in a clustered application. You should use **distributed caching** instead. +* You shouldn't store data in the **local file system**. It should be available to all instances of your application. Different application instance may run in different containers or servers and they may not be able to have access to the same file system. You can use a **cloud or external storage provider** as a solution. +* If you have **background workers** or **job queue managers**, you should be careful since multiple instances may try to execute the same job or perform the same work concurrently. As a result, you may have the same work done multiple times or you may get a lot of errors while trying to access and change the same resources. + +You may have more problems with clustered deployment, but these are the most common ones. ABP has been designed to be compatible with the clustered deployment scenario. The following sections explain what you should do when you are deploying your ABP based application to a clustered environment. + +## Switching to a Distributed Cache + +ASP.NET Core provides different kind of caching features. [In-memory cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory) stores your objects in the memory of the local server and is only available to the application that stored the object. Non-sticky sessions in a clustered environment should use the [distributed caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) except some specific scenarios (for example, you can cache a local CSS file into memory. It is read-only data and it is the same in all application instances. You can cache it in memory for performance reasons without any problem). + +[ABP's Distributed Cache](../Caching.md) extends [ASP.NET Core's distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) infrastructure. It works in-memory by default. You should configure an actual distributed cache provider when you want to deploy your application to a clustered environment. + +> You should configure the cache provider for clustered deployment, even if your application doesn't directly use `IDistributedCache`. Because the ABP Framework and the pre-built [application modules](../Modules/Index.md) are using distributed cache. + +ASP.NET Core provides multiple integrations to use as your distributed cache provider, like [Redis](https://redis.io/) and [NCache](https://www.alachisoft.com/ncache/). You can follow [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to learn how to use them in your applications. + +If you decided to use Redis as your distributed cache provider, **follow [ABP's Redis Cache Integration document](../Redis-Cache.md)** for the steps you need to follow to install it into your application and setup your Redis configuration. + +> Based on your preferences while creating a new ABP solution, Redis cache might be pre-installed in your solution. For example, if you have selected the *Tiered* option with the MVC UI, Redis cache comes as pre-installed. Because, in this case, you have two applications in your solution and they should use the same cache source to be consistent. + +## Using a Proper BLOB Storage Provider + +If you have used ABP's [BLOB Storing](../Blob-Storing.md) feature with the [File System provider](../Blob-Storing-File-System.md), you should use another provider in your clustered environment since the File System provider uses the application's local file system. + +The [Database BLOB provider](../Blob-Storing-Database) is the easiest way since it uses your application's main database (or another database if you configure) to store BLOBs. However, you should remember that BLOBs are large objects and may quickly increase your database's size. + +> [ABP Commercial](https://commercial.abp.io/) startup solution templates come with the database BLOB provider as pre-installed, and stores BLOBs in the application's database. + +Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOG storage providers. + +## Configuring Background Jobs + +ABP's [background job system](../Background-Jobs.md) is used to queue tasks to be executed in the background. Background job queue is persistent and a queued task is guaranteed to be executed (it is re-tried if it fails). + +ABP's default background job manager is compatible with clustered environments. It uses a [distributed lock](../Distributed-Locking.md) to ensure that the jobs are executed only in a single application instance at a time. See the *Configuring a Distributed Lock Provider* section below to learn how to configure a distributed lock provider for your application, so the default background job manager properly works in a clustered environment. + +If you don't want to use a distributed lock provider, you may go with the following options: + +* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs). +* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance. + +> If you are using an external background job integration (e.g. [Hangfire](../Background-Workers-Hangfire.md) or [Quartz](../Background-Workers-Quartz.md)) instead of the default background job manager, then please refer to your provider's documentation to learn how it should be configured for a clustered environment. + +## Configuring a Distributed Lock Provider + +ABP provides a distributed locking abstraction with an implementation made with the [DistributedLock](https://github.com/madelson/DistributedLock) library. A distributed lock is used to control concurrent access to a shared resource by multiple applications to prevent corruption of the resource because of concurrent writes. The ABP Framework and some pre-built [application modules](../Modules/Index.md) are using distributed locking for several reasons. + +However, the distributed lock system works in-process by default. That means it is not distributed actually, unless you configure a distributed lock provider. So, please follow the [distributed lock](../Distributed-Locking.md) document to configure a provider for your application, if it is not already configured. + +## Implementing Background Workers + +ASP.NET Core provides [hosted services](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) and ABP provides [background workers](../Background-Workers.md) to perform tasks in background threads in your application. + +If your application has tasks running in the background, you should consider how they will behave in a clustered environment, especially if your background tasks are using the same resources. You should design your background tasks so that they continue to work properly in the clustered environment. + +Assume that your background worker in your SaaS application checks user subscriptions and sends emails if their subscription renewal date approaches. If the background task runs in multiple application instances, it is probable to send the same email many times to some users, which will disturb them. + +We suggest you to use one of the following approaches to overcome the problem: + +* Implement your background workers so that they work in a clustered environment without any problem. Using the [distributed lock](../Distributed-Locking.md) to ensure concurrency control is a way of doing that. A background worker in an application instance may handle a distributed lock, so the workers in other application instances will wait for the lock. In this way, only one worker does the actual work, while others wait in idle. If you implement this, your workers run safely without caring about how the application is deployed. +* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances except one of them, so only the single instance runs the workers. +* Stop the background workers (set `AbpBackgroundWorkerOptions.IsEnabled` to `false`) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background tasks. This can be a good option if your background workers consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background tasks don't affect your application's performance. \ No newline at end of file diff --git a/docs/en/Deployment/Index.md b/docs/en/Deployment/Index.md new file mode 100644 index 0000000000..dc1cca300e --- /dev/null +++ b/docs/en/Deployment/Index.md @@ -0,0 +1,9 @@ +# Deployment + +Deploying an ABP application is not different than deploying any .NET or ASP.NET Core application. You can deploy it to a cloud provider (e.g. Azure, AWS, Google Could) or on-premise server, IIS or any other web server. ABP's documentation doesn't contain much information on deployment. You can refer to your provider's documentation. + +However, there are some topics that you should care about when you are deploying your applications. Most of them are general software deployment considerations, but you should understand how to handle them within your ABP based applications. We've prepared guides for this purpose and we suggest you to read these guides carefully before designing your deployment configuration. + +## Guides + +* [Deploying to a clustered environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently. \ No newline at end of file diff --git a/docs/en/Distributed-Locking.md b/docs/en/Distributed-Locking.md index d57fd45c83..d29b60ad04 100644 --- a/docs/en/Distributed-Locking.md +++ b/docs/en/Distributed-Locking.md @@ -1,7 +1,5 @@ # Distributed Locking -Distributed locking is a technique to manage many applications that try to access the same resource. -The main purpose is to allow only one of many applications to access the same resource at the same time. -Otherwise, accessing the same object from various applications may corrupt the value of resources. +Distributed locking is a technique to manage many applications that try to access the same resource. The main purpose is to allow only one of many applications to access the same resource at the same time. Otherwise, accessing the same object from various applications may corrupt the value of the resources. > ABP's current distributed locking implementation is based on the [DistributedLock](https://github.com/madelson/DistributedLock) library. @@ -15,7 +13,7 @@ abp add-package Volo.Abp.DistributedLocking This package provides the necessary API to use the distributed locking system, however, you should configure a provider before using it. -### Configuring a provider +### Configuring a Provider The [DistributedLock](https://github.com/madelson/DistributedLock) library provides [various of implementations](https://github.com/madelson/DistributedLock#implementations) for the locking, like [Redis](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md) and [ZooKeeper](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.ZooKeeper.md). @@ -61,7 +59,7 @@ This code gets the Redis connection string from the [configuration](Configuratio There are two ways to use the distributed locking API: ABP's `IAbpDistributedLock` abstraction and [DistributedLock](https://github.com/madelson/DistributedLock) library's API. -### Using the IAbpDistributedLock service +### Using the IAbpDistributedLock Service `IAbpDistributedLock` is a simple service provided by the ABP framework for simple usage of distributed locking. @@ -103,6 +101,10 @@ namespace AbpDemo * `timeout` (`TimeSpan`): A timeout value to wait to obtain the lock. Default value is `TimeSpan.Zero`, which means it doesn't wait if the lock is already owned by another application. * `cancellationToken`: A cancellation token that can be triggered later to cancel the operation. -### Using DistributedLock library's API +### Using DistributedLock Library's API ABP's `IAbpDistributedLock` service is very limited and mainly designed to be internally used by the ABP Framework. For your own applications, you can use the DistributedLock library's own API. See its [own documentation](https://github.com/madelson/DistributedLock) for details. + +## The Volo.Abp.DistributedLocking.Abstractions Package + +If you are building a reusable library or an application module, then you may not want to bring an additional dependency to your module for simple applications that run as a single instance. In this case, your library can depend on the [Volo.Abp.DistributedLocking.Abstractions](https://nuget.org/packages/Volo.Abp.DistributedLocking.Abstractions) package which defines the `IAbpDistributedLock` service and implements it as in-process (not distributed actually). In this way, your library can run properly (without a distributed lock provider dependency) in an application that runs as a single instance. If the application is deployed to a [clustered environment](Deployment/Clustered-Environment.md), then the application developer should install a real distributed provider as explained in the *Installation* section. diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index c5b72a6c1d..0ccd900b1f 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -77,20 +77,17 @@ using BasicAspNetCoreApplication; var builder = WebApplication.CreateBuilder(args); -builder.Services.ReplaceConfiguration(builder.Configuration); - -builder.Services.AddApplication(); +await builder.Services.AddApplicationAsync(); var app = builder.Build(); -app.InitializeApplication(); - -app.Run(); +await app.InitializeApplicationAsync(); +await app.RunAsync(); ```` -``builder.Services.AddApplication();`` adds all services defined in all modules starting from the ``AppModule``. +``builder.Services.AddApplicationAsync();`` adds all services defined in all modules starting from the ``AppModule``. -``app.InitializeApplication()`` initializes and starts the application. +``app.InitializeApplicationAsync()`` initializes and starts the application. ## Run the Application! @@ -98,9 +95,9 @@ That's all! Run the application, it will just work as expected. ## Using Autofac as the Dependency Injection Framework -While AspNet Core's Dependency Injection (DI) system is fine for basic requirements, [Autofac](https://autofac.org/) provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features. +While ASP.NET Core's Dependency Injection (DI) system is fine for basic requirements, [Autofac](https://autofac.org/) provides advanced features like Property Injection and Method Interception which are required by ABP to perform advanced application framework features. -Replacing AspNet Core's DI system by Autofac and integrating to ABP is pretty easy. +Replacing ASP.NET Core's DI system by Autofac and integrating to ABP is pretty easy. 1. Install [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) package @@ -128,19 +125,14 @@ var builder = WebApplication.CreateBuilder(args); builder.Host.UseAutofac(); //Add this line -builder.Services.ReplaceConfiguration(builder.Configuration); - -builder.Services.AddApplication(); +await builder.Services.AddApplicationAsync(); var app = builder.Build(); -app.InitializeApplication(); - -app.Run(); - +await app.InitializeApplicationAsync(); +await app.RunAsync(); ```` ## Source Code Get source code of the sample project created in this tutorial from [here](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication). - diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md index 281add1e2a..fe1d0de246 100644 --- a/docs/en/Getting-Started-Running-Solution.md +++ b/docs/en/Getting-Started-Running-Solution.md @@ -91,10 +91,10 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project** ## Run the Application -> Please execute the `abp install-libs` command to restore the libs required by the web project before running the application. - {{ if UI == "MVC" || UI == "BlazorServer" }} +> Before starting the application, run `abp install-libs` command in your Web directory to restore the client-side libraries. This will populate the `libs` folder. + {{ if Tiered == "Yes" }} > Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. diff --git a/docs/en/Migration-Guides/Abp-5-0-Blazor.md b/docs/en/Migration-Guides/Abp-5-0-Blazor.md index 326bac55d5..09d0f8e682 100644 --- a/docs/en/Migration-Guides/Abp-5-0-Blazor.md +++ b/docs/en/Migration-Guides/Abp-5-0-Blazor.md @@ -1,6 +1,6 @@ # ABP Blazor UI v4.x to v5.0 Migration Guide -> This document is for the ABP MVC / Razor Pages UI. See also [the main migration guide](Abp-5_0.md). +> This document is for the Blazor UI. See also [the main migration guide](Abp-5_0.md). ## Upgrading to the latest Blazorise @@ -16,4 +16,4 @@ Replace `AddBootstrapProviders()` with `AddBootstrap5Providers()`. ## Update Bundle -Use `abp bundle` command to update bundles if you are using Blazor WebAssembly. \ No newline at end of file +Use `abp bundle` command to update bundles if you are using Blazor WebAssembly. diff --git a/docs/en/Migration-Guides/Abp-5_0-Angular.md b/docs/en/Migration-Guides/Abp-5_0-Angular.md index 8099c8acd4..b7e55dad38 100644 --- a/docs/en/Migration-Guides/Abp-5_0-Angular.md +++ b/docs/en/Migration-Guides/Abp-5_0-Angular.md @@ -1,6 +1,6 @@ # Angular UI v4.x to v5.0 Migration Guide -This document is for the ABP MVC / Razor Pages UI. See also [the main migration guide](Abp-5_0.md). +> This document is for the Angular UI. See also [the main migration guide](Abp-5_0.md). ## Overall diff --git a/docs/en/Migration-Guides/Abp-5_0.md b/docs/en/Migration-Guides/Abp-5_0.md index 35780786af..758f2b91ff 100644 --- a/docs/en/Migration-Guides/Abp-5_0.md +++ b/docs/en/Migration-Guides/Abp-5_0.md @@ -117,5 +117,5 @@ db.AbpUsers.updateMany({},{$set:{ IsActive : true }}) ## See Also * [Angular UI 4.x to 5.0 Migration Guide](Abp-5_0-Angular.md) -* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide]() +* [ASP.NET Core MVC / Razor Pages UI 4.x to 5.0 Migration Guide](Abp-5-0-MVC.md) * [Blazor UI 4.x to 5.0 Migration Guide](Abp-5-0-Blazor.md) diff --git a/docs/en/Migration-Guides/Index.md b/docs/en/Migration-Guides/Index.md index e2647d8b0e..17e4d70dd8 100644 --- a/docs/en/Migration-Guides/Index.md +++ b/docs/en/Migration-Guides/Index.md @@ -1,5 +1,7 @@ # ABP Framework Migration Guides +The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../Upgrading.md). + - [5.1 to 5.2](Abp-5_2.md) - [4.x to 5.0](Abp-5_0.md) - [4.2 to 4.3](Abp-4_3.md) diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md index 63cbc31893..dd01f81ff4 100644 --- a/docs/en/Module-Development-Basics.md +++ b/docs/en/Module-Development-Basics.md @@ -29,6 +29,8 @@ public class BlogModule : AbpModule ``ConfigureServices`` is the main method to add your services to the dependency injection system and configure other modules. Example: +> These methods have Async versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. + ````C# public class BlogModule : AbpModule { @@ -57,26 +59,31 @@ public class BlogModule : AbpModule } ```` +> `ConfigureServices` method has an asynchronous version too: `ConfigureServicesAsync`. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one. If you override both asynchronous and synchronous versions, only the asynchronous version will be executed. + See the [Configuration](Configuration.md) document for more about the configuration system. #### Pre & Post Configure Services ``AbpModule`` class also defines ``PreConfigureServices`` and ``PostConfigureServices`` methods to override and write your code just before and just after ``ConfigureServices``. Notice that the code you have written into these methods will be executed before/after the ``ConfigureServices`` methods of all other modules. +> These methods have asynchronous versions too. If you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. + ### Application Initialization Once all the services of all modules are configured, the application starts by initializing all modules. In this phase, you can resolve services from ``IServiceProvider`` since it's ready and available. #### OnApplicationInitialization Method -You can override ``OnApplicationInitialization`` method to execute code while application is being started. Example: +You can override ``OnApplicationInitialization`` method to execute code while application is being started. + +**Example:** ````C# public class BlogModule : AbpModule { - //... - - public override void OnApplicationInitialization(ApplicationInitializationContext context) + public override void OnApplicationInitialization( + ApplicationInitializationContext context) { var myService = context.ServiceProvider.GetService(); myService.DoSomething(); @@ -84,14 +91,32 @@ public class BlogModule : AbpModule } ```` -``OnApplicationInitialization`` is generally used by the startup module to construct the middleware pipeline for ASP.NET Core applications. Example: +`OnApplicationInitialization` method has an asynchronous version too. If you want to make asynchronous calls (use the `await` keyword) inside this method, override the asynchronous version instead of the synchronous one. + +**Example:** + +````csharp +public class BlogModule : AbpModule +{ + public override Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) + { + var myService = context.ServiceProvider.GetService(); + await myService.DoSomethingAsync(); + } +} +```` + +> If you override both asynchronous and synchronous versions, only the asynchronous version will be executed. + +``OnApplicationInitialization`` is generally used by the startup module to construct the middleware pipeline for ASP.NET Core applications. + +**Example:** ````C# [DependsOn(typeof(AbpAspNetCoreMvcModule))] public class AppModule : AbpModule { - //... - public override void OnApplicationInitialization(ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); @@ -113,10 +138,14 @@ You can also perform startup logic if your module requires it ``AbpModule`` class also defines ``OnPreApplicationInitialization`` and ``OnPostApplicationInitialization`` methods to override and write your code just before and just after ``OnApplicationInitialization``. Notice that the code you have written into these methods will be executed before/after the ``OnApplicationInitialization`` methods of all other modules. +> These methods have asynchronous versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones. + ### Application Shutdown Lastly, you can override ``OnApplicationShutdown`` method if you want to execute some code while application is being shutdown. +> This methods has asynchronous version too. If you want to make asynchronous calls inside this method, override the asynchronous version instead of the synchronous one. + ## Module Dependencies In a modular application, it's not unusual for one module to depend upon another module(s). An Abp module must declare ``[DependsOn]`` attribute if it does have a dependency upon another module, as shown below: diff --git a/docs/en/Modules/Cms-Kit/Blogging.md b/docs/en/Modules/Cms-Kit/Blogging.md index 43ab85e129..12039b1669 100644 --- a/docs/en/Modules/Cms-Kit/Blogging.md +++ b/docs/en/Modules/Cms-Kit/Blogging.md @@ -33,7 +33,12 @@ Blog feature uses some of the other CMS Kit features. You can enable or disable You can select/deselect the desired features for blog posts. -![features-dialog](../../images/cmskit-module-features-dialog.png) +![features-dialog](../../images/cmskit-module-features-dialog-2.png) + +##### Quick Navigation Bar In Blog Post +If you enable "Quick navigation bar in blog posts", it will enabled scroll index as seen below. + +![scroll-index](../../images/cmskit-module-features-scroll-index.png) ### Blog Post Management diff --git a/docs/en/Redis-Cache.md b/docs/en/Redis-Cache.md index 3e11584bce..9fe8efdb27 100644 --- a/docs/en/Redis-Cache.md +++ b/docs/en/Redis-Cache.md @@ -21,7 +21,7 @@ abp add-package Volo.Abp.Caching.StackExchangeRedis ## Configuration -Volo.Abp.Caching.StackExchangeRedis package automatically gets the redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: +Volo.Abp.Caching.StackExchangeRedis package automatically gets the Redis [configuration](Configuration.md) from the `IConfiguration`. So, for example, you can set your configuration inside the `appsettings.json`: ````js "Redis": { diff --git a/docs/en/Themes/LeptonXLite/angular.md b/docs/en/Themes/LeptonXLite/angular.md new file mode 100644 index 0000000000..6e4cbef317 --- /dev/null +++ b/docs/en/Themes/LeptonXLite/angular.md @@ -0,0 +1,82 @@ +# LeptonX Lite Angular UI +LeptonX Lite has implementation for the ABP Framework Angular Client. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). + +> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. + +## Installation + +To add `LeptonX-lite` into your project, + +* Install `@abp/ng.theme.lepton-x` + +`yarn add @abp/ng.theme.lepton-x@preview` + +* Install `bootstrap-icons` + +`yarn add bootstrap-icons` + + +* Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one. + +Add the following style + +```json +"node_modules/bootstrap-icons/font/bootstrap-icons.css", +``` + +* Finally, remove `ThemeBasicModule` from `app.module.ts`, and import the related modules in `app.module.ts` + +```js +import { ThemeLeptonXModule } from '@abp/ng.theme.lepton-x'; +import { SideMenuLayoutModule } from '@abp/ng.theme.lepton-x/layouts'; + +@NgModule({ + imports: [ + // ... + + // do not forget to remove ThemeBasicModule + // ThemeBasicModule.forRoot(), + ThemeLeptonXModule.forRoot(), + SideMenuLayoutModule.forRoot(), + ], + // ... +}) +export class AppModule {} +``` + +Note: If you employ [Resource Owner Password Flow](https://docs.abp.io/en/abp/latest/UI/Angular/Authorization#resource-owner-password-flow) for authorization, you should import the following module as well: + +```js +import { AccountLayoutModule } from '@abp/ng.theme.lepton-x/account'; + +@NgModule({ + // ... + imports: [ + // ... + AccountLayoutModule.forRoot(), + // ... + ], + // ... +}) +export class AppModule {} +``` + +To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss` + +```css +:root { + --lpx-logo: url('/assets/images/logo.png'); + --lpx-logo-icon: url('/assets/images/logo-icon.png'); + --lpx-brand: #edae53; +} +``` + +- `--lpx-logo` is used to place the logo in the menu. +- `--lpx-logo-icon` is a square icon used when the menu is collapsed. +- `--lpx-brand` is a color used throughout the application, especially on active elements. + +### Server Side + +In order to migrate to LeptonX on your server side projects (Host and/or IdentityServer projects), please follow the [Server Side Migration](mvc.md) document. diff --git a/docs/en/Themes/LeptonXLite/blazor.md b/docs/en/Themes/LeptonXLite/blazor.md new file mode 100644 index 0000000000..ddea346585 --- /dev/null +++ b/docs/en/Themes/LeptonXLite/blazor.md @@ -0,0 +1,130 @@ +# LeptonX Lite Blazor UI + +````json +//[doc-params] +{ + "UI": ["Blazor", "BlazorServer"] +} +```` + +LeptonX Lite has implementation for the ABP Framework Blazor WebAssembly & Blazor Server. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). + +> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. + +## Installation + +{{if UI == "Blazor"}} +- Complete the [MVC Razor Pages Installation](mvc.md#installation) for the **HttpApi.Host** application first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and if identity server is separated, install to the **IdentityServer**_. + +- Add **Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme** package to your **Blazor WebAssembly** application. + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme + ``` + +- Remove the old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule** type to the **DependsOn** attribute. + +```diff +[DependsOn( +- typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeModule), ++ typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule) +)] +``` + +- Change startup App component with the LeptonX one. + +```csharp +// Make sure the 'App' comes from 'Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite' namespace. +builder.RootComponents.Add("#ApplicationContainer"); +``` + +- Run the `abp bundle` command in your **Blazor** application folder. + +{{end}} + + +{{if UI == "BlazorServer"}} + +- Complete the [MVC Razor Pages Installation](mvc.md#installation) first. _If the solution is tiered/micro-service, complete the MVC steps for all MVC applications such as **HttpApi.Host** and **IdentityServer**_. + +- Add **Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme** package to your **Blazor server** application. + ```bash + dotnet add package Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme + ``` + +- Remove old theme from the **DependsOn** attribute in your module class and add the **AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeModule** type to the **DependsOn** attribute. + + ```diff + [DependsOn( + - typeof(AbpAspNetCoreComponentsServerBasicThemeModule), + + typeof(AbpAspNetCoreComponentsServerLeptonXLiteThemeModule) + )] + ``` + +- Update AbpBundlingOptions + ```diff + options.StyleBundles.Configure( + - BlazorBasicThemeBundles.Styles.Global, + + BlazorLeptonXLiteThemeBundles.Styles.Global, + bundle => + { + bundle.AddFiles("/blazor-global-styles.css"); + //You can remove the following line if you don't use Blazor CSS isolation for components + bundle.AddFiles("/MyProjectName.Blazor.styles.css"); + }); + ``` + +- Update `_Host.cshtml` file. _(located under **Pages** folder by default.)_ + + - Add following usings to Locate **App** and **BlazorLeptonXLiteThemeBundles** classes. + ```csharp + @using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite + @using Volo.Abp.AspNetCore.Components.Server.LeptonXLiteTheme.Bundling + ``` + - Then replace script & style bundles as following: + ```diff + - + + + ``` + + ```diff + - + + + ``` + +{{end}} + + +--- + +## Customization + +### Toolbars +LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. + +- `LeptonXLiteToolbars.Main` +- `LeptonXLiteToolbars.MainMobile` + +```csharp +public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) +{ + if (context.Toolbar.Name == LeptonXLiteToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); + } + + if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); + } + + return Task.CompletedTask; +} +``` + +{{if UI == "BlazorServer"}} + +> _You can visit the [Toolbars Documentation](https://docs.abp.io/en/abp/latest/UI/Blazor/Toolbars) for better understanding._ + +{{end}} diff --git a/docs/en/Themes/LeptonXLite/mvc.md b/docs/en/Themes/LeptonXLite/mvc.md new file mode 100644 index 0000000000..b5575cba49 --- /dev/null +++ b/docs/en/Themes/LeptonXLite/mvc.md @@ -0,0 +1,67 @@ +# LeptonX Lite MVC UI +LeptonX Lite has implementation for the ABP Framework Razor Pages. It's a simplified variation of the [LeptonX Theme](https://x.leptontheme.com/). + +> If you are looking for a professional, enterprise ready theme, you can check the [LeptonX Theme](https://x.leptontheme.com/), which is a part of [ABP Commercial](https://commercial.abp.io/). + +> See the [Theming document](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming) to learn about themes. + +## Installation + +- Add **Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite** package to your **Web** application. + +```bash +abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite +``` + +- Make sure the old theme is removed and LeptonX is added in your Module class. + +```diff +[DependsOn( +- typeof(AbpAspNetCoreMvcUiBasicThemeModule), ++ typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule) +)] +``` + +- Update AbpBundlingOptions + +```diff +Configure(options => +{ + options.StyleBundles.Configure( +- BasicThemeBundles.Styles.Global, ++ LeptonXLiteThemeBundles.Styles.Global + bundle => + { + bundle.AddFiles("/global-styles.css"); + } + ); +}); +``` + +--- + +## Customization + +### Toolbars +LeptonX Lite includes separeted toolbars for desktop & mobile. You can manage toolbars independently. Toolbar names can be accessible in the **LeptonXLiteToolbars** class. + +- `LeptonXLiteToolbars.Main` +- `LeptonXLiteToolbars.MainMobile` + +```csharp +public class MyProjectNameMainToolbarContributor : IToolbarContributor +{ + public async Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == LeptonXLiteToolbars.Main) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyDesktopComponent))); + } + + if (context.Toolbar.Name == LeptonXLiteToolbars.MainMobile) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(MyMobileComponent))); + } + } +} +``` diff --git a/docs/en/UI/Angular/DateTime-Format-Pipe.md b/docs/en/UI/Angular/DateTime-Format-Pipe.md new file mode 100644 index 0000000000..4841ffe665 --- /dev/null +++ b/docs/en/UI/Angular/DateTime-Format-Pipe.md @@ -0,0 +1,29 @@ +# DateTime Format Pipes + +You can format date by Date pipe of angular. + +Example + +```html + {{today | date 'dd/mm/yy'}} +``` + +ShortDate, ShortTime and ShortDateTime format data like angular's data pipe but easier. Also the pipes get format from config service by culture. + +# ShortDate Pipe + +```html + {{today | shortDatePipe }} +``` + +# ShortTime Pipe + +```html + {{today | shortTimePipe }} +``` + +# ShortDateTime Pipe + +```html + {{today | shortDateTimePipe }} +``` diff --git a/docs/en/UI/Angular/Page-Toolbar-Extensions.md b/docs/en/UI/Angular/Page-Toolbar-Extensions.md index 7e63950224..1edc9594fb 100644 --- a/docs/en/UI/Angular/Page-Toolbar-Extensions.md +++ b/docs/en/UI/Angular/Page-Toolbar-Extensions.md @@ -21,9 +21,9 @@ The following code prepares a constant named `identityToolbarActionContributors` import { eIdentityComponents, - IdentityToolbarActionContributors, - IdentityUserDto, + IdentityToolbarActionContributors } from '@abp/ng.identity'; +import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { ToolbarAction, ToolbarActionList } from '@abp/ng.theme.shared/extensions'; const logUserNames = new ToolbarAction({ @@ -93,7 +93,7 @@ We need to have a component before we can pass it to the toolbar action contribu ```js // src/app/click-me-button.component.ts -import { IdentityUserDto } from '@abp/ng.identity'; +import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.theme.shared/extensions'; import { Component, Inject } from '@angular/core'; @@ -127,9 +127,9 @@ The following code prepares a constant named `identityToolbarActionContributors` import { eIdentityComponents, - IdentityToolbarActionContributors, - IdentityUserDto, + IdentityToolbarActionContributors } from '@abp/ng.identity'; +import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { ToolbarActionList, ToolbarComponent } from '@abp/ng.theme.shared/extensions'; import { ClickMeButtonComponent } from './click-me-button.component'; @@ -362,7 +362,7 @@ export function reorderUserContributors( ) { // drop "New User" button const newUserActionNode = actionList.dropByValue( - 'AbpIdentity::NewUser', + 'AbpIdentity::NewUser', (action, text) => action['text'] === text, ); diff --git a/docs/en/_deleted/Bootstrap-Modules.md b/docs/en/_deleted/Bootstrap-Modules.md new file mode 100644 index 0000000000..904197e4e3 --- /dev/null +++ b/docs/en/_deleted/Bootstrap-Modules.md @@ -0,0 +1,128 @@ +# Bootstrap Modules + +ABP framework can be bootstrapped in various applications. like ASP NET Core or Console, WPF, etc. + +Modules can be started sync or async, Corresponds to various event methods in the module. We recommend using **async**, Especially if you want to make async calls in the module's methods. + +The `PreConfigureServices`, `ConfigureServices`, `PostConfigureServices`, `OnPreApplicationInitialization`, `OnApplicationInitialization`, `OnPostApplicationInitialization` methods of module have the Async version. + +> Async methods automatically call sync methods by default. + +If you use async methods in your module, please keep the same sync methods for compatibility. + +````csharp +public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) +{ + await AsyncMethod(); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context)); +} +```` + +## ASP NET Core + +Bootstrap ABP by using [WebApplication](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.webapplication?view=aspnetcore-6.0) + +````csharp +var builder = WebApplication.CreateBuilder(args); +builder.Host.AddAppSettingsSecretsJson() + .UseAutofac(); + +await builder.Services.AddApplicationAsync(); +var app = builder.Build(); + +await app.InitializeApplicationAsync(); +await app.RunAsync(); +```` + +## Console + +Bootstrap ABP in Console App. + +````csharp +var abpApplication = await AbpApplicationFactory.CreateAsync(options => +{ + options.UseAutofac(); +}); + +await _abpApplication.InitializeAsync(); + +var helloWorldService = _abpApplication.ServiceProvider.GetRequiredService(); + +await helloWorldService.SayHelloAsync(); +```` + +Bootstrap ABP by using [HostedService](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio#ihostedservice-interface) + +````csharp +public class MyHostedService : IHostedService +{ + private IAbpApplicationWithInternalServiceProvider _abpApplication; + + private readonly IConfiguration _configuration; + private readonly IHostEnvironment _hostEnvironment; + + public MyHostedService(IConfiguration configuration, IHostEnvironment hostEnvironment) + { + _configuration = configuration; + _hostEnvironment = hostEnvironment; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _abpApplication = await AbpApplicationFactory.CreateAsync(options => + { + options.Services.ReplaceConfiguration(_configuration); + options.Services.AddSingleton(_hostEnvironment); + + options.UseAutofac(); + options.Services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog()); + }); + + await _abpApplication.InitializeAsync(); + + var helloWorldService = _abpApplication.ServiceProvider.GetRequiredService(); + + await helloWorldService.SayHelloAsync(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _abpApplication.ShutdownAsync(); + } +} +```` + +## WPF + +Bootstrap ABP on [OnStartup](https://docs.microsoft.com/en-us/dotnet/api/system.windows.application.onstartup?view=windowsdesktop-6.0) method. + +````csharp +public partial class App : Application +{ + private IAbpApplicationWithInternalServiceProvider _abpApplication; + + protected async override void OnStartup(StartupEventArgs e) + { + _abpApplication = await AbpApplicationFactory.CreateAsync(options => + { + options.UseAutofac(); + options.Services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); + }); + + await _abpApplication.InitializeAsync(); + + _abpApplication.Services.GetRequiredService()?.Show(); + } + + protected async override void OnExit(ExitEventArgs e) + { + await _abpApplication.ShutdownAsync(); + } +} + +```` + diff --git a/docs/en/_resources/Diagrams.pptx b/docs/en/_resources/Diagrams.pptx new file mode 100644 index 0000000000..05edf2ced7 Binary files /dev/null and b/docs/en/_resources/Diagrams.pptx differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 072413fe80..786a790c13 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -704,6 +704,10 @@ { "text": "The Basic Theme", "path": "UI/AspNetCore/Basic-Theme.md" + }, + { + "text": "LeptonX Lite", + "path": "Themes/LeptonXLite/mvc.md" } ] }, @@ -817,6 +821,10 @@ "text": "The Basic Theme", "path": "UI/Blazor/Basic-Theme.md" }, + { + "text": "The Basic Theme", + "path": "Themes/LeptonXLite/blazor.md" + }, { "text": "Branding", "path": "UI/Blazor/Branding.md" @@ -1071,6 +1079,10 @@ { "text": "The Basic Theme", "path": "UI/Angular/Basic-Theme.md" + }, + { + "text": "LeptonX Lite", + "path": "Themes/LeptonXLite/angular.md" } ] }, @@ -1085,10 +1097,10 @@ { "text": "Extensions", "items": [ - { + { "text": "Overall", "path": "UI/Angular/Extensions-Overall.md" - }, + }, { "text": "Entity Action Extensions", "path": "UI/Angular/Entity-Action-Extensions.md" @@ -1104,6 +1116,10 @@ { "text": "Dynamic Form Extensions", "path": "UI/Angular/Dynamic-Form-Extensions.md" + }, + { + "text": "Date, time and datetime format pipes", + "path": "UI/Angular/DateTime-Format-Pipe.md" } ] } @@ -1226,15 +1242,12 @@ "path": "Testing.md" }, { - "text": "Samples", + "text": "Deployment", + "path": "Deployment/Index.md", "items": [ { - "text": "All Samples", - "path": "Samples/Index.md" - }, - { - "text": "Microservice Demo", - "path": "Samples/Microservice-Demo.md" + "text": "Deploying to a Clustered Environment", + "path": "Deployment/Clustered-Environment.md" } ] }, @@ -1299,6 +1312,19 @@ } ] }, + { + "text": "Samples", + "items": [ + { + "text": "All Samples", + "path": "Samples/Index.md" + }, + { + "text": "Microservice Demo", + "path": "Samples/Microservice-Demo.md" + } + ] + }, { "text": "Release Information", "items": [ diff --git a/docs/en/images/cmskit-module-features-dialog-2.png b/docs/en/images/cmskit-module-features-dialog-2.png new file mode 100644 index 0000000000..b6dd409e0a Binary files /dev/null and b/docs/en/images/cmskit-module-features-dialog-2.png differ diff --git a/docs/en/images/cmskit-module-features-scroll-index.png b/docs/en/images/cmskit-module-features-scroll-index.png new file mode 100644 index 0000000000..0bc3dedcc5 Binary files /dev/null and b/docs/en/images/cmskit-module-features-scroll-index.png differ diff --git a/docs/en/images/deployment-clustered.png b/docs/en/images/deployment-clustered.png new file mode 100644 index 0000000000..74177482c5 Binary files /dev/null and b/docs/en/images/deployment-clustered.png differ diff --git a/docs/en/images/deployment-single-instance.png b/docs/en/images/deployment-single-instance.png new file mode 100644 index 0000000000..8c3f8719b2 Binary files /dev/null and b/docs/en/images/deployment-single-instance.png differ diff --git a/docs/zh-Hans/CLI-New-Command-Samples.md b/docs/zh-Hans/CLI-New-Command-Samples.md new file mode 100644 index 0000000000..3adfa472ed --- /dev/null +++ b/docs/zh-Hans/CLI-New-Command-Samples.md @@ -0,0 +1,248 @@ +# ABP CLI - 新解决方案命令示例 + +`abp new`命令基于abp模板创建abp解决方案或其他组件. [ABP CLI](CLI.md)有一些参数可以用于创建新的ABP解决方案. 在本文档中, 我们将向你展示一些创建新的解决方案的命令示例. 所有的项目名称都是`Acme.BookStore`. 目前, 唯一可用的移动端项目是`React Native`移动端应用程序. 可用的数据库提供程序有`Entity Framework Core`和`MongoDB`. 所有命令都以`abp new`开头. + +## Angular + +以下命令用于创建Angular UI项目: + +* 在新文件夹中创建项目, **Entity Framework Core**, 非移动端应用程序: + + ````bash + abp new Acme.BookStore -u angular --mobile none --database-provider ef -csf + ```` + +* 在新文件夹中创建项目, **Entity Framework Core**, 默认应用程序模板, **拆分Identity Server**: + + ```bash + abp new Acme.BookStore -t app -u angular -m none --separate-identity-server --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **Entity Framework Core**, **自定义连接字符串**: + + ```bash + abp new Acme.BookStore -u angular -csf --connection-string Server=localhost;Database=MyDatabase;Trusted_Connection=True + ``` + +* 在`C:\MyProjects\Acme.BookStore`中创建解决方案, **MongoDB**, 默认应用程序模板, 包含移动端项目: + + ```bash + abp new Acme.BookStore -u angular --database-provider mongodb --output-folder C:\MyProjects\Acme.BookStore + ``` + +* 在新文件夹中创建项目, **MongoDB**, 默认应用程序模板, 不创建移动端应用程序, **拆分Identity Server**: + + ```bash + abp new Acme.BookStore -t app -u angular -m none --separate-identity-server --database-provider mongodb -csf + ``` + +## MVC + +以下命令用于创建MVC UI项目: + +* 在新文件夹中创建项目, **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **Entity Framework Core**, **分层结构** (*Web和HTTP API层是分开的*), 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u mvc --mobile none --tiered --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider mongodb -csf + ``` + +* 在新文件夹中创建项目, **MongoDB**, **分层结构**: + + ```bash + abp new Acme.BookStore -u mvc --tiered --database-provider mongodb -csf + ``` + + +## Blazor + +以下命令用于创建Blazor项目: + +* **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u blazor --mobile none + ``` + +* **Entity Framework Core**, **拆分Identity Server**, 包含移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --separate-identity-server + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf + ``` + +## Blazor Server + +以下命令用于创建Blazor项目: + +* **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u blazor-server --mobile none + ``` + +* **Entity Framework Core**, **拆分Identity Server**, **拆分API Host**, 包含移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor-server --tiered + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf + ``` + +## 无UI + +在默认应用程序模板中, 始终有一个前端项目. 在这个选项中没有前端项目. 它有一个`HttpApi.Host`项目为你的HTTP WebAPI提供服务. 这个选项适合在你想创建一个WebAPI服务时使用. + +* 在新文件夹中创建项目, **Entity Framework Core**, 拆分Identity Server: + + ```bash + abp new Acme.BookStore -u none --separate-identity-server -csf + ``` +* **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u none --mobile none --database-provider mongodb + ``` + + + +## 控制台应用程序 + +这是一个基于.NET控制台应用程序的模板, 集成了ABP模块架构. 要创建控制台应用程序, 请使用以下命令: + +* 项目由以下文件组成: `Acme.BookStore.csproj`, `appsettings.json`, `BookStoreHostedService.cs`, `BookStoreModule.cs`, `HelloWorldService.cs` 和 `Program.cs`. + + ```bash + abp new Acme.BookStore -t console -csf + ``` + +## 模块 + +模块是主项目使用的可重用子应用程序. 如果你正在构建微服务解决方案, 使用ABP模块是最佳方案. 由于模块不是最终的应用程序, 每个模块都有前端UI项目和数据库提供程序. 模块模板带有MVC UI, 可以在没有最终解决方案的情况下进行开发. 但是, 如果要在最终解决方案下开发模块, 可以添加`--no-ui`参数来去除MVC UI项目. + +* 包含前端: `MVC`, `Angular`, `Blazor`. 包含数据库提供程序: `Entity Framework Core`, `MongoDB`. 包含MVC启动项目. + + ```bash + abp new Acme.IssueManagement -t module + ``` +* 与上面相同, 但不包括MVC启动项目. + + ```bash + abp new Acme.IssueManagement -t module --no-ui + ``` + +* 创建模块并将其添加到解决方案中 + + ```bash + abp new Acme.IssueManagement -t module --add-to-solution-file + ``` + +## 从特定版本创建解决方案 + +创建解决方案时, 它总是使用最新版本创建. 要从旧版本创建项目, 可以使用`--version`参数. + +* 使用v3.3.0版本创建解决方案, 包含Angular UI和Entity Framework Core. + + ```bash + abp new Acme.BookStore -t app -u angular -m none --database-provider ef -csf --version 3.3.0 + ``` + +要获取ABP版本列表, 请查看以下链接: https://www.nuget.org/packages/Volo.Abp.Core/ + +## 从自定义模板创建 + +ABP CLI使用默认的[应用程序模板](https://github.com/abpframework/abp/tree/dev/templates/app)创建项目. 如果要从自定义模板创建新的解决方案, 可以使用参数`--template-source`. + +* 在`c:\MyProjects\templates\app`目录中使用模板, MVC UI, Entity Framework Core, 不创建移动端应用程序. + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source "c:\MyProjects\templates\app" + ``` + +* 除了此命令从URL `https://myabp.com/app-template.zip` 检索模板之外, 与上一个命令相同. + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source https://myabp.com/app-template.zip + ``` + +## 创建预览版本 + +ABP CLI始终使用最新版本. 要从预览(RC)版本创建解决方案, 请添加`--preview`参数. + +* 在新文件夹中创建项目, Blazor UI, Entity Framework Core, 不创建移动端应用程序, **使用最新版本**: + + ```bash + abp new Acme.BookStore -t app -u blazor --mobile none -csf --preview + ``` + +## 选择数据库管理系统 + +默认的数据库管理系统是 `Entity Framework Core` / ` SQL Server`. 你可以通过使用`--database-management-system`参数选择DBMS. [可用的值](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/DatabaseManagementSystem.cs) 包括 `SqlServer`, `MySQL`, `SQLite`, `Oracle`, `Oracle-Devart`, `PostgreSQL`. 默认值是 `SqlServer`. + +* 在新文件夹中创建项目, Angular UI, **PostgreSQL** 数据库: + + ```bash + abp new Acme.BookStore -u angular --database-management-system PostgreSQL -csf + ``` + +## 使用静态HTTP端口 + +ABP CLI始终为项目分配随机端口. 如果需要保留默认端口并且创建解决方案始终使用相同的HTTP端口, 请添加参数`--no-random-port`. + +* 在新文件夹中创建项目, MVC UI, Entity Framework Core, **静态端口**: + + ```bash + abp new Acme.BookStore --no-random-port -csf + ``` + +## 引用本地ABP框架 + +在ABP解决方案中, 默认情况下从NuGet引用ABP库. 有时, 你需要在本地将ABP库引用到你的解决方案中. 这利于调试框架本身. 本地ABP框架的根目录必须有`Volo.Abp.sln`文件. 你可以将以下目录的内容复制到你的文件系统中 + +* MVC UI, Entity Framework Core, **引用本地的ABP库**: + +本地路径必须是ABP存储库的根目录. +如果`C:\source\abp\framework\Volo.Abp.sln`是你的框架解决方案的路径, 那么你必须设置`--abp-path`参数值为`C:\source\abp`. + + ```bash + abp new Acme.BookStore --local-framework-ref --abp-path C:\source\abp + ``` + +**输出**: + +如下所示, 引用本地ABP框架库项目. + +```xml + + + + + + + + +``` + +## 另请参阅 + +* [ABP CLI文档](CLI.md) diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 5ff00c5501..bef5a7220e 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -78,6 +78,8 @@ abp new Acme.BookStore * Acme.BookStore是解决方案的名称. * 常见的命名方式类似于 *YourCompany.YourProject*. 不过你可以使用自己喜欢的方式,如 *YourProject* (单级命名空间) 或 *YourCompany.YourProduct.YourModule* (三级命名空间). +参阅[ABP CLI 创建新解决方案示例](CLI-New-Command-Samples.md)查看更多示例. + #### Options * `--template` 或者 `-t`: 指定模板. 默认的模板是 `app`,会生成web项目.可用的模板有: @@ -407,4 +409,4 @@ abp install-libs [options] #### Options -* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. \ No newline at end of file +* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. diff --git a/docs/zh-Hans/Connection-Strings.md b/docs/zh-Hans/Connection-Strings.md index cf3aed11fa..290cce0436 100644 --- a/docs/zh-Hans/Connection-Strings.md +++ b/docs/zh-Hans/Connection-Strings.md @@ -7,7 +7,7 @@ ABP框架的设计是[模块化](Module-Development-Basics.md), [微服务兼容 它还支持混合场景; -* 允许将模块分组到数据库 (所有的模块分组到一个共享数据库, 两个模块使用数据库A, 3个模块使用数据库B, 一个数据库使用数据库C其余的数据库使用数据库D...等.) +* 允许将模块分组到数据库 (所有的模块分组到一个共享数据库, 两个模块使用数据库A, 三个模块使用数据库B, 一个模块使用数据库C其余的模块使用数据库D...等.) * 允许将租户分组到数据库中,像模块一样. * 允许为每个租户每个模块分离数据库 (数据库过多会增加维护成本,但ABP框架支持这种需求). diff --git a/docs/zh-Hans/Deployment/Clustered-Environment.md b/docs/zh-Hans/Deployment/Clustered-Environment.md new file mode 100644 index 0000000000..34547709d1 --- /dev/null +++ b/docs/zh-Hans/Deployment/Clustered-Environment.md @@ -0,0 +1,99 @@ +# 部署到群集环境 + +本文档介绍了在将应用程序部署到**多个应用程序实例同时运行**的集群环境中时应注意的内容, 并解释了如何在基于ABP的应用程序中处理这些内容. + +> 无论你使用的是单体式应用程序还是微服务解决方案, 本文档均有效. 适用于一个流程. 应用程序可以是单体式web应用程序、微服务解决方案中的服务、控制台应用程序或其他类型的可执行进程. +> +> 例如, 如果你将应用程序部署到Kubernetes并把应用程序或服务在多个POD中运行, 那么应用程序或服务将在集群环境中运行. + +## 了解集群环境 + +> 如果你已经熟悉集群部署和负载均衡器, 可以跳过本节. + +### 单实例部署 + +考虑作为**单个实例**部署的应用程序, 如下图所示: + +![deployment-single-instance](../images/deployment-single-instance.png) + +浏览器和其他客户端应用程序可以直接向应用程序发出HTTP请求. 你可以在客户端和应用程序之间放置一个web服务器(例如IIS或NGINX), 但仍有一个应用程序实例在单个服务器或容器中运行. 单实例的配置**限于规模**, 因为它在一台服务器上运行, 并且你受到服务器容量的限制. + +### 集群部署 + +**集群部署**是在一台或多台服务器上**同时运行**应用程序**多个实例**的方式. 通过这种方式, 不同的实例可以满足不同的请求, 并且可以通过在系统中添加新服务器来扩展. 下图显示了集群使用**负载均衡器**的典型实现: + +![deployment-clustered](../images/deployment-clustered.png) + +### 负载均衡器 + +[负载均衡器](https://en.wikipedia.org/wiki/Load_balancing_(computing)) 有很多特性, 但它们基本上会将**传入的HTTP请求转发**给应用程序的实例, 并将响应返回给客户端应用程序. + +负载平衡器可以使用不同的算法来选择应用程序实例, 同时确定用于传递传入请求的应用程序实例. **循环**是最简单、最常用的算法之一. 请求被轮流传递到应用程序实例. 第一个实例得到第一个请求, 第二个实例得到第二个请求, 依此类推. 在所有实例都被使用之后, 它返回到第一个实例, 并且下一个请求的算法也是类似的. + +### 潜在问题 + +一旦应用程序的多个实例并行运行, 你应该仔细考虑以下内容: + +* 当你有多个实例时, 存储在应用程序 **内存中的任何状态(数据)** 都将成为问题. 存储在应用程序实例内存中的状态可能在下一个请求中不可用, 因为下一个请求将由不同的应用程序实例处理. 虽然有一些解决方案(比如粘性会话)可以解决这个问题, 但如果你想在集群、容器或云中运行应用程序, **最好将其设计为无状态**. +* **内存缓存** 是一种内存状态, 不应在集群应用程序中使用. 你应该使用**分布式缓存**. +* 你不应该在**本地文件系统**中存储应用程序所有实例都可以使用的数据. 不同的应用程序实例可能在不同的容器或服务器中运行, 并且它们可能无法访问同一个文件系统. 你可以使用**云或外部存储提供商**作为解决方案. +* 如果你有**后台工作者**或**作业队列管理器**, 则应小心, 因为多个实例可能会尝试执行同一作业或同时执行同一工作. 因此, 你可能会多次完成相同的工作, 或者在尝试访问和更改相同的资源时可能会出现很多错误. + +集群部署可能会有更多问题, 但这些是最常见的问题. ABP被设计为与集群部署场景兼容. 以下各节介绍了将基于ABP的应用程序部署到集群环境时应执行的操作. + +## 切换分布式缓存 + +ASP.NET Core提供了不同类型的缓存功能. [内存缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory)将对象存储在本地服务器的内存中, 并且仅对存储该对象的应用程序可用. 集群环境中的非粘性会话应使用[分布式缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed), 除了一些特定场景(例如, 你可以将本地CSS文件缓存到内存中. 它是只读数据, 在所有应用程序实例中都是相同的. 出于性能原因, 你可以将其缓存到内存中, 而不会出现任何问题). + +[ABP的分布式缓存](../Caching.md)扩展了[ASP.NET Core的分布式缓存](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)的基础设施. 默认情况下, 它在内存中工作. 当你要将应用程序部署到集群环境时, 应该配置实际的分布式缓存提供程序. + +> 即使应用程序不直接使用`IDistributedCache`, 也应该为集群部署配置缓存提供程序. 因为ABP框架和预构建的[应用程序模块](../Modules/Index.md)正在使用分布式缓存. + +ASP.NET Core提供了可以用作分布式缓存提供程序的多种集成, 如[Redis](https://redis.io/)和[NCache](https://www.alachisoft.com/ncache/). 你可以按照[微软文档](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)了解如何在应用程序中使用它们. + +如果你决定使用Redis作为分布式缓存提供程序, **请遵循[ABP的Redis缓存集成文档](../Redis-Cache.md)** 了解将其安装到应用程序并配置Redis所需遵循的步骤. + +> 根据你在创建新ABP解决方案时的偏好, Redis缓存可能会预先安装在你的解决方案中. 例如, 如果你在MVC UI中选择了*Tiered*选项, Redis缓存将进行预装. 因为, 在这种情况下, 解决方案中有两个应用程序, 它们应该使用相同的缓存源来保持一致. + +## 使用合适的BLOB存储提供程序 + +如果你在[文件系统提供程序](../Blob-Storing-File-System.md)中使用了ABP的[BLOB存储](../Blob-Storing.md)功能, 则应该在集群环境中使用另一个提供程序, 因为文件系统提供程序使用应用程序的本地文件系统. + +[数据库BLOB提供程序](../Blob-Storing-Database)是最简单的方法, 因为它使用应用程序的主数据库(或另一个数据库, 如果你配置的话)来存储BLOB. 但是, 你应该记住, BLOB是大型对象, 可能会迅速增加数据库的大小. + +> [ABP商业版](https://commercial.abp.io/)启动解决方案模板预装了数据库BLOB提供程序, 并将BLOB存储在应用程序的数据库中. + +查看[BLOB Storing](../Blob-Storing.md)文档以查看所有可用的BLOB存储提供程序. + +## 配置后台作业 + +ABP的[后台作业系统](../Background-Jobs.md)将要在后台执行的任务进行排队. 后台作业队列是持久性的, 排队的任务能够保证执行(如果失败, 将重新尝试). + +ABP的默认后台作业管理器与集群环境兼容. 它使用[分布式锁](../Distributed-Locking.md)来确保一次只能在单个应用程序实例中执行作业. 请参阅下面的*配置分布式锁提供程序*部分, 了解如何为应用程序配置分布式锁提供程序, 以便默认后台作业管理器在集群环境中正常工作. + +如果不想使用分布式锁提供程序, 可以使用以下选项: + +* 停止所有应用程序实例中的后台作业管理器(将`AbpBackgroundJobOptions.IsJobExecutionEnabled`设置为`false`)只保留其中一个应用程序实例, 以便只有单个实例执行作业(而其他应用程序实例仍可以对作业进行排队). +* 在所有应用程序实例中停止后台作业管理器(将`AbpBackgroundJobOptions.IsJobExecutionEnabled`设置为`false`), 并创建一个专用的应用程序(可能是在自己的容器中运行的控制台应用程序或在后台运行的Windows服务)来执行所有后台作业. 如果你的后台作业占用大量系统资源(CPU、RAM或磁盘), 那么这是一个不错的选择, 这样你就可以将该后台应用程序部署到专用服务器上, 并且后台作业不会影响应用程序的性能. + +> 如果你使用的是外部后台作业集成(例如[Hangfire](../Background-Workers-Hangfire.md)或[Quartz](../Background-Workers-Quartz.md))而不是默认的后台作业管理器, 请参阅提供程序的文档, 了解如何为集群环境配置它. + +## 配置分布式锁提供程序 + +ABP通过[分布式锁](https://github.com/madelson/DistributedLock)库实现了一个抽象的分布式锁. 分布式锁用于控制多个应用程序对共享资源的并发访问, 以防止由于并发写入而导致资源损坏. ABP框架和一些预构建的[应用程序模块](../Modules/Index.md)出于一些原因正在使用分布式锁. + +但是, 分布式锁系统默认在进程中工作. 这意味着它实际上不是分布式的, 除非配置分布式锁提供程序. 因此, 如果尚未配置应用程序的提供程序, 请按照[分布式锁](../Distributed-Locking.md)文档为其配置提供程序. + +## 实现后台工作者 + +ASP.NET Core[托管服务](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services), ABP提供[后台工作者](../Background-Workers.md)在应用程序的后台线程中执行任务. + +如果你的应用程序有在后台运行的任务, 你应该注意它们在集群环境中的行为, 尤其是在后台任务使用相同资源的情况下. 你应该设计后台任务, 以便它们在集群环境中继续正常工作. + +假设SaaS应用程序中的后台工作者检查用户订阅, 并在订阅续订日期临近时发送电子邮件. 如果后台任务在多个应用程序实例中运行, 可能会多次向某些用户发送同一封电子邮件, 这会影响他们. + +我们建议你使用以下方法之一来解决此问题: + +* 实现你的后台工作者, 以便他们在集群环境中工作时不会出现任何问题. 使用[分布式锁](../Distributed-Locking.md)来确保并发控制是一种方法. 应用程序实例中的后台工作者可能会处理分布式锁, 因此其他应用程序实例中的工作者将等待该锁. 这样, 只有一个工作者在实际工作, 而其他的则在等待. 如果你实现了这一点, 你的后台工作者就可以安全地运行, 不必关心应用程序是如何部署的. +* 停止所有应用程序实例中的后台工作者(将`AbpBackgroundWorkerOptions.IsEnabled`设置为`false`), 只保留其中一个应用程序实例, 因此只有单个实例运行这些后台工作者. +* 停止所有应用程序实例中的后台工作者(将`AbpBackgroundWorkerOptions.IsEnabled`设置为`false`), 并创建一个专用的应用程序(可能是在自己的容器中运行的控制台应用程序或在后台运行的Windows服务)来执行所有后台任务. 如果你的后台工作者消耗大量系统资源(CPU、RAM或磁盘), 那么这是一个不错的选择, 这样你就可以将该后台应用程序部署到专用服务器上, 并且你的后台任务不会影响应用程序的性能. diff --git a/docs/zh-Hans/Deployment/Index.md b/docs/zh-Hans/Deployment/Index.md new file mode 100644 index 0000000000..9df24e56f0 --- /dev/null +++ b/docs/zh-Hans/Deployment/Index.md @@ -0,0 +1,9 @@ +# 部署 + +部署ABP应用程序与部署其他.NET或ASP.NET Core应用程序并没有什么不同. 你可以将其部署到云服务提供商(例如Azure、AWS、Google)或内部部署服务器、IIS或任何其他web服务器. ABP的文档中没有太多关于部署的信息. 你可以参考提供商的文档. + +但是, 在部署应用程序时, 有些主题是你应该注意的. 其中大多数是一般的软件部署注意事项, 但你应该了解如何在基于ABP的应用程序中处理它们. 我们为此准备了指南, 建议你在设计部署配置之前仔细阅读这些指南. + +## 指南 + +* [部署到群集环境](Clustered-Environment.md): 讲解了当你希望同时运行应用程序的多个实例时, 如何来配置应用程序. diff --git a/docs/zh-Hans/Distributed-Locking.md b/docs/zh-Hans/Distributed-Locking.md new file mode 100644 index 0000000000..2176230403 --- /dev/null +++ b/docs/zh-Hans/Distributed-Locking.md @@ -0,0 +1,110 @@ +# 分布式锁 +分布式锁是一种管理多个应用程序访问同一资源的技术. 主要目的是同一时间只允许多个应用程序中的一个访问资源. 否则, 从不同的应用程序访问同一对象可能会破坏资源. + +> ABP当前的分布式锁实现基于[DistributedLock](https://github.com/madelson/DistributedLock)库. + +## 安装 + +你可以打开一个命令行终端并输入以下命令来安装[Volo.Abp.DistributedLocking](https://www.nuget.org/packages/Volo.Abp.DistributedLocking)到你的项目中: + +````bash +abp add-package Volo.Abp.DistributedLocking +```` + +这个库提供了使用分布式锁系统所需的API, 但是, 在使用它之前, 你应该配置一个提供程序. + +### 配置一个提供程序 + +[DistributedLock](https://github.com/madelson/DistributedLock)库对[Redis](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md)和[ZooKeeper](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.ZooKeeper.md)提供[多种实现](https://github.com/madelson/DistributedLock#implementations). + +例如, 如果你想使用[Redis provider](https://github.com/madelson/DistributedLock/blob/master/docs/DistributedLock.Redis.md), 你应该将[DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet包添加到项目中, 然后将以下代码添加到ABP[模块](Module-Development-Basics.md)类的`ConfigureServices`方法中: + +````csharp +using Medallion.Threading; +using Medallion.Threading.Redis; + +namespace AbpDemo +{ + [DependsOn( + typeof(AbpDistributedLockingModule) + //If you have the other dependencies, you should do here + )] + public class MyModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + context.Services.AddSingleton(sp => + { + var connection = ConnectionMultiplexer + .Connect(configuration["Redis:Configuration"]); + return new + RedisDistributedSynchronizationProvider(connection.GetDatabase()); + }); + } + } +} +```` + +此代码从[配置](Configuration.md)获取Redis连接字符串, 因此你可以将以下行添加到`appsettings.json`文件: + +````json +"Redis": { + "Configuration": "127.0.0.1" +} +```` + +## 使用 + +有两种方法可以使用分布式锁API: ABP的`IAbpDistributedLock`抽象和[DistributedLock](https://github.com/madelson/DistributedLock)库的API. + +### 使用IAbpDistributedLock服务 + +`IAbpDistributedLock`是ABP框架提供的一个用于简单使用分布式锁的服务. + +**实例: 使用`IAbpDistributedLock.TryAcquireAsync`方法** + +````csharp +using Volo.Abp.DistributedLocking; + +namespace AbpDemo +{ + public class MyService : ITransientDependency + { + private readonly IAbpDistributedLock _distributedLock; + public MyService(IAbpDistributedLock distributedLock) + { + _distributedLock = distributedLock; + } + + public async Task MyMethodAsync() + { + await using (var handle = + await _distributedLock.TryAcquireAsync("MyLockName")) + { + if (handle != null) + { + // your code that access the shared resource + } + } + } + } +} +```` + +`TryAcquireAsync`可能无法获取锁. 如果无法获取锁, 则返回`null`. 在这种情况下, 你不应该访问资源. 如果句柄不为`null`, 则表示你已获得锁, 并且可以安全地访问资源. + +`TryAcquireAsync`方法拥有以下参数: + +* `name` (`string`, 必须): 锁的唯一名称. 不同的锁命名用于访问不同的资源. +* `timeout` (`TimeSpan`): 等待获取锁的超时值. 默认值为`TimeSpan.Zero`, 这意味着如果锁已经被另一个应用程序拥有, 它不会等待. +* `cancellationToken`: 取消令牌可在触发后取消操作. + +### 使用DistributedLock库的API + +ABP的`IAbpDistributedLock`服务非常有限, 主要用于ABP框架的内部使用. 对于你自己的应用程序, 可以使用DistributedLock库自己的API. 参见[文档](https://github.com/madelson/DistributedLock)详细信息. + +## Volo.Abp.DistributedLocking.Abstractions库 + +如果你正在构建一个可重用的库或应用程序模块, 那么对于作为单个实例运行的简单应用程序, 你可能不希望为模块带来额外的依赖关系. 在这种情况下, 你的库可以依赖于[Volo.Abp.DistributedLocking.Abstractions](https://nuget.org/packages/Volo.Abp.DistributedLocking.Abstractions)库, 它定义了`IAbpDistributedLock`服务, 并将其在进程内实现(实际上不是分布式的). 通过这种方式, 你的库可以在作为单个实例运行的应用程序中正常运行(没有分布式锁提供程序依赖项). 如果应用程序部署到[集群环境](Deployment/Clustered-Environment.md), 那么应用程序开发人员应该安装一个真正的分布式提供程序, 如*安装*部分所述. diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index e54258d61f..7cd96b2820 100644 --- a/docs/zh-Hans/Entities.md +++ b/docs/zh-Hans/Entities.md @@ -28,7 +28,7 @@ public class Book : Entity * 创建一个构造函数,获取ID作为参数传递给基类. * 如果没有为GUID Id赋值,**ABP框架会在保存时设置它**,但是在将实体保存到数据库之前最好在实体上有一个有效的Id. * 如果使用带参数的构造函数创建实体,那么还要创建一个 `private` 或 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它. -* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中**使用[`IGuidGenerator`服务](Guid-Generation.md)**传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. +* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中**使用[`IGuidGenerator`服务](Guid-Generation.md)** 传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. 示例实体: diff --git a/docs/zh-Hans/Settings.md b/docs/zh-Hans/Settings.md index a4d84c91b7..73d3115b19 100644 --- a/docs/zh-Hans/Settings.md +++ b/docs/zh-Hans/Settings.md @@ -65,7 +65,7 @@ public class MySettingDefinitionProvider : SettingDefinitionProvider } ```` -> 使用常量作为设置名称是一种好习惯,ABP的包就是这样做的. `Abp.Mailing.Smtp`设置名称是在`EmailSettingNames`类(在Volo.Abp.Emailing名称空间中)定义的常量. +> 使用常量作为设置名称是一种好习惯,ABP的包就是这样做的. `Abp.Mailing.Smtp.Host`设置名称是在`EmailSettingNames`类(在Volo.Abp.Emailing命名空间中)定义的常量. ## 读取设置值 @@ -141,7 +141,7 @@ var requireDigit = abp.setting.getBoolean('Abp.Identity.Password.RequireDigit'); * `TenantSettingValueProvider`: 获取当前租户的设置值(参阅 [多租户](Multi-Tenancy.md)文档). * `UserSettingValueProvider`: 获取当前用户的设置值(参阅 [当前用户](CurrentUser.md) 文档). -> 设置回退系统从底部 (用户) 到 (默认) 方向起用用. +> 设置回退系统从底部 (用户) 到 顶部(默认) 方向起作用. 全局,租户和用户设置值提供程序使用 `ISettingStore` 从数据源读取值(参见下面的小节). @@ -222,7 +222,7 @@ Configure(options => `ISettingEncryptionService` 用于在设置定义的 `isencryption` 属性设置为 `true` 时加密/解密设置值. -你可以在依赖项入系统中替换此服务,自定义实现加密/解密过程. 默认实现 `StringEncryptionService` 使用AES算法(参见字符串[加密文档](String-Encryption.md)学习更多). +你可以在依赖注入系统中替换此服务,自定义实现加密/解密过程. 默认实现 `StringEncryptionService` 使用AES算法(参见字符串[加密文档](String-Encryption.md)学习更多). ## 设置管理模块 diff --git a/docs/zh-Hans/Testing.md b/docs/zh-Hans/Testing.md new file mode 100644 index 0000000000..5bb72803c8 --- /dev/null +++ b/docs/zh-Hans/Testing.md @@ -0,0 +1,768 @@ +# 自动化测试 + +## 介绍 + +ABP框架的设计考虑了可测试性. 有一些不同级别的自动化测试: + +* **单元测试**: 通常只测试一个类(或者一起测试几个类). 这些测试会很快. 然而, 你通常需要处理对服务依赖项的模拟. +* **集成测试**: 你通常会测试一个服务, 但这一次你不会模拟基本的基础设施和服务, 以查看它们是否正确地协同工作. +* **用户界面测试**: 测试应用程序的UI, 就像用户与应用程序交互一样. + +### 单元测试 vs 集成测试 + +与单元测试相比, 集成测试有一些显著的**优势**: + +* **编写更加简单** 因为你不需要模拟和处理依赖关系. +* 你的测试代码运行于所有真正的服务和基础设施(包括数据库映射和查询), 因此它更接近于**真正的应用程序测试**. + +同时它们有一些缺点: + +* 与单元测试相比, 它们**更慢**, 因为所有的基础设施都准备好了测试用例. +* 服务中的一个bug可能会导致多个测试用例失败, 因此在某些情况下, 可能会**更难找到真正的问题**. + +我们建议混合使用: 在必要的地方编写单元测试或集成测试, 并且有效的编写和维护它. + +## 应用程序启动模板 + +测试基础设施提供[应用程序启动模板](Startup-Templates/Application.md) , 并已经正确安装和配置. + +### 测试项目 + +请参见Visual Studio中的以下解决方案: + +![solution-test-projects](images/solution-test-projects.png) + +按层级系统分为多个测试项目: + +* `Domain.Tests` 用于测试领域层对象 (例如[领域服务](Domain-Services.md) 和 [实体](Entities.md)). +* `Application.Tests` 用于测试应用层对象 (例如[应用服务](Application-Services.md)). +* `EntityFrameworkCore.Tests` 用于测试你的自定义仓储实现或EF Core映射(如果你使用其他[数据访问](Data-Access.md))的话, 该项目将有所不同). +* `Web.Tests` 用于测试UI层(如页面、控制器和视图组件). 该项目仅适用于MVC / Razor页面应用程序. +* `TestBase` 包含一些由其他项目共享/使用的类. + +> `HttpApi.Client.ConsoleTestApp` 不是自动化测试的应用程序. 它是一个示例的控制台应用程序, 展示了如何从.NET控制台应用程序中调用HTTP API. + +以下的部分将介绍这些项目中包含的基类和其他基础设施. + +### 测试基础设施 + +解决方案中已经安装了以下库: + +* [xUnit](https://xunit.net/) 作为测试框架. +* [NSubstitute](https://nsubstitute.github.io/) 用于模拟. +* [Shouldly](https://github.com/shouldly/shouldly) 用于断言. + +虽然你可以用自己喜欢的工具替换它们, 但本文档和示例将基于这些工具. + +## 测试资源管理器 + +你可以在Visual Studio中使用测试资源管理器查看和运行测试. 其他IDE, 请参阅它们自己的文档. + +### 打开测试资源管理器 + +打开*测试*菜单下的*测试资源管理器*(如果尚未打开): + +![vs-test-explorer](images/vs-test-explorer.png) + +### 运行测试 + +然后, 你可以单击在视图中运行所有测试或运行按钮来运行测试. 初始启动模板为你提供了一些测试用例: + +![vs-startup-template-tests](images/vs-startup-template-tests.png) + +### 并行运行测试 + +支持并行运行测试. **强烈建议**并行运行所有测试, 这比逐个运行测试要快得多. + +要启用它, 请单击设置(齿轮)按钮附近的插入符号图标, 然后选择*并行运行测试*. + +![vs-run-tests-in-parallel](images/vs-run-tests-in-parallel.png) + +## 单元测试 + +对于单元测试, 不需要太多的配置. 通常会实例化你的类, 并对要测试的对象提供一些预先配置的模拟对象. + +### 没有依赖项的类 + +要测试的类没有依赖项是最简单的情况, 你可以直接实例化类, 调用其方法并做出断言. + +#### 示例: 测试实体 + +假设你有一个 `Issue` [实体](Entities.md), 如下所示: + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace MyProject.Issues +{ + public class Issue : AggregateRoot + { + public string Title { get; set; } + public string Description { get; set; } + public bool IsLocked { get; set; } + public bool IsClosed { get; private set; } + public DateTime? CloseDate { get; private set; } + + public void Close() + { + IsClosed = true; + CloseDate = DateTime.UtcNow; + } + + public void Open() + { + if (!IsClosed) + { + return; + } + + if (IsLocked) + { + throw new IssueStateException("You can not open a locked issue!"); + } + + IsClosed = true; + CloseDate = null; + } + } +} + +```` + +请注意, `IsClosed`和`CloseDate`属性具有私有setter, 可以使用`Open()`和`Close()`方法强制执行某些业务逻辑: + +* 无论何时关闭issue, `CloseDate`都应设置为[当前时间](Timing.md). +* 如果issue被锁定, 则无法重新打开. 如果它被重新打开, `CloseDate`应该设置为`null`. + +由于`Issue`实体是领域层的一部分, 所以我们应该在`Domain.Tests`项目中测试它. 在`Domain.Tests`项目中创建一个`Issue_Tests`类: + +````csharp +using Shouldly; +using Xunit; + +namespace MyProject.Issues +{ + public class Issue_Tests + { + [Fact] + public void Should_Set_The_CloseDate_Whenever_Close_An_Issue() + { + // Arrange + + var issue = new Issue(); + issue.CloseDate.ShouldBeNull(); // null at the beginning + + // Act + + issue.Close(); + + // Assert + + issue.IsClosed.ShouldBeTrue(); + issue.CloseDate.ShouldNotBeNull(); + } + } +} +```` + +这个测试遵循AAA(Arrange-Act-Assert)模式: + +* **Arrange** 部分创建一个`Issue`实体, 并确保`CloseDate`在初始值为`null`. +* **Act** 部分执行我们想要测试的方法. +* **Assert** 部分检查`Issue`属性是否与我们预期的相同. + +`[Fact]`属性由[xUnit](https://xunit.net/)并将方法标记为测试方法. `Should...`扩展方法由[Shouldly](https://github.com/shouldly/shouldly)提供. 你可以直接使用xUnit中的`Assert`类, 使用Shouldly让它更舒适、更直观. + +当你执行测试时, 你将看到它成功通过: + +![issue-first-test](images/issue-first-test.png) + +让我们再添加两种测试方法: + +````csharp +[Fact] +public void Should_Allow_To_ReOpen_An_Issue() +{ + // Arrange + + var issue = new Issue(); + issue.Close(); + + // Act + + issue.Open(); + + // Assert + + issue.IsClosed.ShouldBeFalse(); + issue.CloseDate.ShouldBeNull(); +} + +[Fact] +public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() +{ + // Arrange + + var issue = new Issue(); + issue.Close(); + issue.IsLocked = true; + + // Act & Assert + + Assert.Throws(() => + { + issue.Open(); + }); +} +```` + +`Assert.Throws` 检查执行的代码是否匹配引发的异常. + +> 有关这些库的更多信息, 请参阅xUnit & Shoudly的文档. + +### 具有依赖项的类 + +如果你的服务中有依赖项, 并且你想对该服务进行单元测试, 那么你需要模拟这些依赖项. + +#### 示例: 测试领域服务 + +假设你有一个`IssueManager` [领域服务](Domain-Services.md), 定义如下: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace MyProject.Issues +{ + public class IssueManager : DomainService + { + public const int MaxAllowedOpenIssueCountForAUser = 3; + + private readonly IIssueRepository _issueRepository; + + public IssueManager(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignToUserAsync(Issue issue, Guid userId) + { + var issueCount = await _issueRepository.GetIssueCountOfUserAsync(userId); + + if (issueCount >= MaxAllowedOpenIssueCountForAUser) + { + throw new BusinessException( + code: "IM:00392", + message: $"You can not assign more" + + $"than {MaxAllowedOpenIssueCountForAUser} issues to a user!" + ); + } + + issue.AssignedUserId = userId; + } + } +} +```` + +`IssueManager`依赖于`IssueRepository`服务, 在本例中将模拟该服务. + +**业务逻辑**: 示例`AssignToUserAsync`不允许向用户分配超过3个issue (`MaxAllowedOpenIssueCountForAUser`常量). 在这种情况下, 如果要分配issue, 首先需要取消现有issue的分配. + +下面的测试用例给出一个有效的赋值: + +````csharp +using System; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Tests + { + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Arrange + + var userId = Guid.NewGuid(); + + var fakeRepo = Substitute.For(); + fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1); + + var issueManager = new IssueManager(fakeRepo); + + var issue = new Issue(); + + // Act + + await issueManager.AssignToUserAsync(issue, userId); + + //Assert + + issue.AssignedUserId.ShouldBe(userId); + await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); + } + } +} +```` + +* `Substitute.For` 创建一个模拟(假)对象, 该对象被传递到`IssueManager`构造函数中. +* `fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1)` 确保仓储中的`GetIssueContofuseRasync`方法返回`1`. +* `issueManager.AssignToUserAsync` 不会引发任何异常, 因为仓储统计当前分配的issue数量并且返回`1`. +* `issue.AssignedUserId.ShouldBe(userId);` 行检查`AssignedUserId`的值是否正确. +* `await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);` 检查 `IssueManager` 实际只调用了 `GetIssueCountOfUserAsync` 方法一次. + +让我们添加第二个测试, 看看它是否能阻止将issue分配给超过分配数量的用户: + +````csharp +[Fact] +public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() +{ + // Arrange + + var userId = Guid.NewGuid(); + + var fakeRepo = Substitute.For(); + fakeRepo + .GetIssueCountOfUserAsync(userId) + .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); + + var issueManager = new IssueManager(fakeRepo); + + // Act & Assert + + var issue = new Issue(); + + await Assert.ThrowsAsync(async () => + { + await issueManager.AssignToUserAsync(issue, userId); + }); + + issue.AssignedUserId.ShouldBeNull(); + await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); +} +```` + +> 有关模拟的更多信息, 请参阅[NSubstitute](https://nsubstitute.github.io/)文档. + +模拟单个依赖项相对容易. 但是, 当依赖关系增长时, 设置测试对象和模拟所有依赖关系变得越来越困难. 请参阅不需要模拟依赖项的*Integration Tests*部分. + +### 提示: 共享测试类构造函数 + +[xUnit](https://xunit.net/) 为每个测试方法创建一个**新测试类实例**(本例中为`IssueManager_Tests`). 因此, 你可以将一些*Arrange*代码移动到构造函数中, 以减少代码重复. 构造函数将针对每个测试用例执行, 并且不会相互影响, 即使它们是并行工作. + +**示例: 重构`IssueManager_Tests`以减少代码重复** + +````csharp +using System; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Tests + { + private readonly Guid _userId; + private readonly IIssueRepository _fakeRepo; + private readonly IssueManager _issueManager; + private readonly Issue _issue; + + public IssueManager_Tests() + { + _userId = Guid.NewGuid(); + _fakeRepo = Substitute.For(); + _issueManager = new IssueManager(_fakeRepo); + _issue = new Issue(); + } + + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Arrange + _fakeRepo.GetIssueCountOfUserAsync(_userId).Returns(1); + + // Act + await _issueManager.AssignToUserAsync(_issue, _userId); + + //Assert + _issue.AssignedUserId.ShouldBe(_userId); + await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); + } + + [Fact] + public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() + { + // Arrange + _fakeRepo + .GetIssueCountOfUserAsync(_userId) + .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _issueManager.AssignToUserAsync(_issue, _userId); + }); + + _issue.AssignedUserId.ShouldBeNull(); + await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); + } + } +} +```` + +> 保持测试代码整洁, 以创建可维护的测试组件. + +## 集成测试 + +> 你还可以按照[Web应用程序开发教程](Tutorials/Part-1.md)学习开发全栈应用程序, 包括集成测试. + +### 集成测试基础 + +ABP为编写集成测试提供了完整的基础设施. 所有ABP基础设施和服务都将在你的测试中执行. 应用程序启动模板附带了为你预先配置的必要基础设施; + +#### 数据库 + +启动模板使用EF Core配置**内存中的SQLite**数据库(对于MongoDB, 它使用[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)). 因此, 所有配置和查询都是针对真实数据库执行的, 你甚至可以测试数据库事务. + +使用内存中的SQLite数据库有两个主要优点: + +* 它比外部DBMS更快. +* 它会为每个测试用例创建一个**新的数据库**, 这样测试就不会相互影响. + +> **提示**: 不要将EF Core的内存数据库用于高级集成测试. 它不是一个真正的DBMS, 在细节上有很多不同. 例如, 它不支持事务和回滚场景, 因此无法真正测试失败的场景. 另一方面, 内存中的SQLite是一个真正的DBMS, 支持SQL数据库的基本功能. + +### 种子数据 + +针对空数据库编写测试是不现实的. 在大多数情况下, 需要在数据库中保存一些初始数据. 例如, 如果你编写了一个查询、更新和删除产品的测试类, 那么在执行测试用例之前, 在数据库中有一些产品数据会很有帮助. + +ABP的[种子数据](Data-Seeding.md)系统是一种强大的初始化数据的方法. 应用程序启动模板在`.TestBase`项目中有一个*YourProject*TestDataSeedContributor类. 你可以在其中添加, 以获得可用于每个测试方法的初始数据. + +**示例: 创建一些Issue作为种子数据** + +````csharp +using System.Threading.Tasks; +using MyProject.Issues; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace MyProject +{ + public class MyProjectTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IIssueRepository _issueRepository; + + public MyProjectTestDataSeedContributor(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue one", + Description = "Test issue one description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue two", + Description = "Test issue two description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue three", + Description = "Test issue three description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue four", + Description = "Test issue four description", + AssignedUserId = TestData.User2Id + }); + } + } +} +```` + +还创建了一个静态类来存储用户的 `Id`: + +````csharp +using System; + +namespace MyProject +{ + public static class TestData + { + public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B"); + public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999"); + } +} +```` + +通过这种方式, 我们可以使用这些已知Issue和用户的`Id`来运行测试. + +### 示例: 测试领域服务 + +`AbpIntegratedTest`类 (定义在[Volo.Abp.TestBase](https://www.nuget.org/packages/Volo.Abp.TestBase)) 用于编写集成到ABP框架的测试. `T`是用于设置和初始化应用程序的根模块的类型. + +应用程序启动模板在每个测试项目中都有基类, 因此你可以从这些基类派生, 以使其更简单. + +`IssueManager`测试将被重写成集成测试 + +````csharp +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Integration_Tests : MyProjectDomainTestBase + { + private readonly IssueManager _issueManager; + private readonly Issue _issue; + + public IssueManager_Integration_Tests() + { + _issueManager = GetRequiredService(); + _issue = new Issue + { + Title = "Test title", + Description = "Test description" + }; + } + + [Fact] + public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() + { + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _issueManager.AssignToUserAsync(_issue, TestData.User1Id); + }); + + _issue.AssignedUserId.ShouldBeNull(); + } + + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Act + await _issueManager.AssignToUserAsync(_issue, TestData.User2Id); + + //Assert + _issue.AssignedUserId.ShouldBe(TestData.User2Id); + } + } +} +```` + +* 第一个测试方法将issue分配给User1, 其中User1已经分配了种子数据代码中的3个issue. 因此, 它抛出了一个`BusinessException`. +* 第二种测试方法将issue分配给User2, User2只分配了一个issue. 因此, 该方法成功了. + +这个类通常位于`.Domain.Tests`项目中, 因为它测试位于`.Domain`项目中的类. 它派生自`MyProjectDomainTestBase`, 并已经为正确运行测试进行了配置. + +编写这样一个集成测试类非常简单. 另一个好处是, 在以后向`IssueManager`类添加另一个依赖项时, 不需要更改测试类. + +### 示例: 测试应用服务 + +测试[应用服务](Application-Services.md)并没有太大的不同. 假设你已经创建了一个`IssueAppService`, 定义如下: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace MyProject.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IIssueRepository _issueRepository; + + public IssueAppService(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task> GetListAsync() + { + var issues = await _issueRepository.GetListAsync(); + + return ObjectMapper.Map, List>(issues); + } + } +} +```` + +*(假设你还定义了`IIssueAppService`和`IssueDto`, 并在`Issue`和`IssueDto`之间创建了[对象映射](Object-To-Object-Mapping.md))* + +现在, 你可以在`.Application.Tests`项目中编写一个测试类: + +````csharp +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueAppService_Tests : MyProjectApplicationTestBase + { + private readonly IIssueAppService _issueAppService; + + public IssueAppService_Tests() + { + _issueAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_All_Issues() + { + //Act + var issueDtos = await _issueAppService.GetListAsync(); + + //Assert + issueDtos.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +就这么简单. 此测试方法测试的所有内容, 包括应用服务、EF Core映射、对象到对象映射和仓储实现. 通过这种方式, 你可以完全测试解决方案的应用层和领域层. + +### 处理集成测试中的工作单元 + +ABP的[工作单元](Unit-Of-Work.md)系统控制应用程序中的数据库连接和事务管理. 它可以在你编写应用程序代码时无缝工作, 因此你可能没有意识到它. + +在ABP框架中, 所有数据库操作都必须在一个工作单元作用域内执行. 当你测试[应用服务](Application-Services.md)方法时, 工作单元的作用域将是应用服务方法的作用域. 如果你正在测试[仓储](Repositories.md)方法, 那么工作单元作用域将是你的仓储方法的作用域. + +在某些情况下, 你可能需要手动控制工作单元作用域. 可以考虑下面的测试方法: + +````csharp +public class IssueRepository_Tests : MyProjectDomainTestBase +{ + private readonly IRepository _issueRepository; + + public IssueRepository_Tests() + { + _issueRepository = GetRequiredService>(); + } + + public async Task Should_Query_By_Title() + { + IQueryable queryable = await _issueRepository.GetQueryableAsync(); + var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); + issue.ShouldNotBeNull(); + } +} +```` + +我们正在使用`_issueRepository.GetQueryableAsync`获取`IQueryable` 对象. 然后, 我们使用`FirstOrDefaultAsync`方法按标题查询issue. 此时执行数据库查询, 你将会得到一个异常, 表明没有起作用的工作单元. + +要使该测试正常工作, 你应该手动启动工作单元作用域, 如下所示: + +````csharp +public class IssueRepository_Tests : MyProjectDomainTestBase +{ + private readonly IRepository _issueRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public IssueRepository_Tests() + { + _issueRepository = GetRequiredService>(); + _unitOfWorkManager = GetRequiredService(); + } + + public async Task Should_Query_By_Title() + { + using (var uow = _unitOfWorkManager.Begin()) + { + IQueryable queryable = await _issueRepository.GetQueryableAsync(); + var issue = queryable.FirstOrDefaultAsync(i => i.Title == "My issue title"); + issue.ShouldNotBeNull(); + await uow.CompleteAsync(); + } + } +} +```` + +我们已经使用了`IUnitOfWorkManager`服务来创建一个工作单元作用域, 然后在该作用域内调用了`FirstOrDefaultAsync`方法, 所以不再有问题了. + +> 请注意, 我们测试了`FirstOrDefaultAsync`来演示工作单元的问题. 作为一个好的标准, 编写自己的代码. + +### 使用DbContext + +在某些情况下, 你可能希望使用Entity Framework的`DbContext`对象来执行测试方法中的数据库操作. 在这种情况下, 可以使用`IDbContextProvider`服务在工作单元内获取`DbContext`实例. + +下面的示例展示了如何在测试方法中创建`DbContext`对象: + +````csharp +public class MyDbContext_Tests : MyProjectDomainTestBase +{ + private readonly IDbContextProvider _dbContextProvider; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public IssueRepository_Tests() + { + _dbContextProvider = GetRequiredService>(); + _unitOfWorkManager = GetRequiredService(); + } + + public async Task Should_Query_By_Title() + { + using (var uow = _unitOfWorkManager.Begin()) + { + var dbContext = await _dbContextProvider.GetDbContextAsync(); + var issue = await dbContext.Issues.FirstOrDefaultAsync(i => i.Title == "My issue title"); + issue.ShouldNotBeNull(); + await uow.CompleteAsync(); + } + } +} +```` + +就像我们在*集成测试中处理工作单元*一节中所做的那样, 我们应该在起作用的工作单元内执行`DbContext`操作. + +对于[MongoDB](MongoDB.md), 你可以使用`IMongoDbContextProvider`服务获取`DbContext`对象, 并在测试方法中直接使用MongoDB APIs. + +## 用户界面测试 + +一般来说, 有两种类型的UI测试: + +### 非可视化测试 + +此类测试完全取决于UI框架的选择: + +* 对于MVC / Razor页面UI, 通常向服务器发出请求, 获取HTML, 并测试返回的结果中是否存在一些预期的DOM元素. +* Angular有自己的基础设施和实践来测试组件、视图和服务. + +请参阅以下文档以了解非可视化UI测试: + +* [Testing in ASP.NET Core MVC / Razor Pages](UI/AspNetCore/Testing.md) +* [Testing in Angular](UI/Angular/Testing.md) +* [Testing in Blazor](UI/Blazor/Testing.md) + +### 可视化测试 + +与真实用户一样, 可视化测试用于与应用程序UI交互. 它全面测试应用程序, 包括页面和组件的外观. + +可视化UI测试超出了ABP框架的范围. 行业中有很多工具(比如[Selenium](https://www.selenium.dev/))可以用来测试应用程序的UI. diff --git a/docs/zh-Hans/UI/Angular/Testing.md b/docs/zh-Hans/UI/Angular/Testing.md new file mode 100644 index 0000000000..c176d82247 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Testing.md @@ -0,0 +1,380 @@ +# Angular UI 单元测试 + +ABP Angular UI的测试与其他Angular应用程序一样. 所以, [这里的指南](https://angular.io/guide/testing)也适用于ABP. 也就是说, 我们想指出一些**特定于ABP Angular应用程序的单元测试内容**. + +## 设置 + +在Angular中, 单元测试默认使用[Karma](https://karma-runner.github.io/)和[Jasmine](https://jasmine.github.io). 虽然我们更喜欢Jest, 但我们选择不偏离这些默认设置, 因此**你下载的应用程序模板将预先配置Karma和Jasmine**. 你可以在根目录中的 _karma.conf.js_ 文件中找到Karma配置. 你什么都不用做. 添加一个spec文件并运行`npm test`即可. + +## 基础 + +简化版的spec文件如下所示: + +```js +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MyComponent], + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +如果你看一下导入内容, 你会注意到我们已经准备了一些测试模块来取代内置的ABP模块. 这对于模拟某些特性是必要的, 否则这些特性会破坏你的测试. 请记住**使用测试模块**并**调用其`withConfig`静态方法**. + +## 提示 + +### Angular测试库 + +虽然你可以使用Angular TestBed测试代码, 但你可以找到一个好的替代品[Angular测试库](https://testing-library.com/docs/angular-testing-library/intro). + +上面的简单示例可以用Angular测试库编写, 如下所示: + +```js +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { render } from "@testing-library/angular"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + const result = await render(MyComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }); + + fixture = result.fixture; + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +正如你所见, 二者非常相似. 当我们使用查询和触发事件时, 真正的区别就显现出来了. + +```js +// other imports +import { getByLabelText, screen } from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("author-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + }); +}); +``` + +**Angular测试库中的查询遵循可维护测试**, 用户事件库提供了与DOM的**类人交互**, 并且该库通常有**清晰的API**简化组件测试. 下面提供一些有用的链接: + +- [查询](https://testing-library.com/docs/dom-testing-library/api-queries) +- [用户事件](https://testing-library.com/docs/ecosystem-user-event) +- [范例](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples) + +### 在每个Spec之后清除DOM + +需要记住的一点是, Karma在真实的浏览器实例中运行测试. 这意味着, 你将能够看到测试代码的结果, 但也会遇到与文档正文连接的组件的问题, 这些组件可能无法在每次测试后都清除, 即使你配置了Karma也一样无法清除. + +我们准备了一个简单的函数, 可以在每次测试后清除所有剩余的DOM元素. + +```js +// other imports +import { clearPage } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(MyComponent, { + /* removed for sake of brevity */ + }); + fixture = result.fixture; + }); + + // specs here +}); +``` + +请确保你使用它, 否则Karma将无法删除对话框, 并且你将有多个模态对话框、确认框等的副本. + +### 等待 + +一些组件, 特别是在检测周期之外工作的模态对话框. 换句话说, 你无法在打开这些组件后立即访问这些组件插入的DOM元素. 同样, 插入的元素在关闭时也不会立即销毁. + +为此, 我们准备了一个`wait`函数. + +```js +// other imports +import { wait } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should open a modal", async () => { + const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); + userEvent.click(openModalBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + + expect(modal).toBeTruthy(); + + /* wait again after closing the modal */ + }); +}); +``` + +`wait`函数接受第二个参数, 即超时(默认值为`0`). 但是尽量不要使用它. 使用大于`0`的超时通常表明某些不正确事情发生了. + +## 测试示例 + +下面是一个测试示例. 它并没有涵盖所有内容, 但却能够对测试有一个更好的了解. + +```js +import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, +} from "@ng-bootstrap/ng-bootstrap"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { CountryService } from "@proxy/countries"; +import { + findByText, + getByLabelText, + getByRole, + getByText, + queryByRole, + render, + screen, +} from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; +import { BehaviorSubject, of } from "rxjs"; +import { CountryComponent } from "./country.component"; + +const list$ = new BehaviorSubject({ + items: [{ id: "ID_US", name: "United States of America" }], + totalCount: 1, +}); + +describe("Country", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(CountryComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, + ], + providers: [ + { + provide: CountryService, + useValue: { + getList: () => list$, + }, + }, + ], + }); + + fixture = result.fixture; + }); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("country-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + + userEvent.click(advancedFiltersBtn); + expect(nameInput.offsetWidth).toBe(0); + }); + + it("should have a heading", () => { + const heading = screen.getByRole("heading", { name: "Countries" }); + expect(heading).toBeTruthy(); + }); + + it("should render list in table", async () => { + const table = await screen.findByTestId("country-table"); + + const name = getByText(table, "United States of America"); + expect(name).toBeTruthy(); + }); + + it("should display edit modal", async () => { + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const editBtn = screen.getByRole("button", { name: /edit/i }); + userEvent.click(editBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); + expect(modalHeading).toBeTruthy(); + + const closeBtn = getByText(modal, "×"); + userEvent.click(closeBtn); + + await wait(fixture); + + expect(screen.queryByRole("dialog")).toBeFalsy(); + }); + + it("should display create modal", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /new/i }); + + expect(modalHeading).toBeTruthy(); + }); + + it("should validate required name field", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const nameInput = getByRole(modal, "textbox", { + name: /^name/i, + }) as HTMLInputElement; + + userEvent.type(nameInput, "x"); + userEvent.type(nameInput, "{backspace}"); + + const nameError = await findByText(modal, /required/i); + expect(nameError).toBeTruthy(); + }); + + it("should delete a country", () => { + const getSpy = spyOn(fixture.componentInstance.list, "get"); + const deleteSpy = jasmine.createSpy().and.returnValue(of(null)); + fixture.componentInstance.service.delete = deleteSpy; + + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const deleteBtn = screen.getByRole("button", { name: /delete/i }); + userEvent.click(deleteBtn); + + const confirmText = screen.getByText("AreYouSure"); + expect(confirmText).toBeTruthy(); + + const confirmBtn = screen.getByRole("button", { name: "Yes" }); + userEvent.click(confirmBtn); + + expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); + expect(getSpy).toHaveBeenCalledTimes(1); + }); +}); +``` + +## CI配置 + +你的CI环境需要不同的配置. 要为单元测试设置新的配置, 请在测试项目中找到 _angular.json_ 文件, 或者如下所示添加一个: + +```json +// angular.json + +"test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { /* several options here */ }, + "configurations": { + "production": { + "karmaConfig": "karma.conf.prod.js" + } + } +} +``` + +现在你可以复制 _karma.conf.js_ 作为 _karma.conf.prod.js_ 并在其中使用你喜欢的任何配置. 请查看[Karma配置文档](http://karma-runner.github.io/5.2/config/configuration-file.html)配置选项. + +最后, 不要忘记使用以下命令运行CI测试: + +```sh +npm test -- --prod +``` + +## 另请参阅 + +- [ABP Community Video - Unit Testing with the Angular UI](https://community.abp.io/articles/unit-testing-with-the-angular-ui-p4l550q3) diff --git a/docs/zh-Hans/UI/AspNetCore/Testing.md b/docs/zh-Hans/UI/AspNetCore/Testing.md new file mode 100644 index 0000000000..e5b09353fb --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Testing.md @@ -0,0 +1,220 @@ +# ASP.NET Core MVC / Razor Pages: 测试 + +> 你可以参考[ASP.NET Core集成测试文档](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests)了解ASP.NET Core集成测试的详细内容. 本文档解释了ABP框架提供的附加测试基础设施. + +## 应用程序启动模板 + +应用程序启动模板的`.Web`项目其中包含应用程序的UI视图/页面/组件, 并提供`.Web.Tests`项目来测试这些内容. + +![aspnetcore-web-tests-in-solution](../../images/aspnetcore-web-tests-in-solution.png) + +## 测试Razor页面 + +假设你已经创建了一个名为`Issues.cshtml`的Razor页面, 包含以下内容; + +**Issues.cshtml.cs** + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MyProject.Issues; + +namespace MyProject.Web.Pages +{ + public class IssuesModel : PageModel + { + public List Issues { get; set; } + + private readonly IIssueAppService _issueAppService; + + public IssuesModel(IIssueAppService issueAppService) + { + _issueAppService = issueAppService; + } + + public async Task OnGetAsync() + { + Issues = await _issueAppService.GetListAsync(); + } + } +} +```` + +**Issues.cshtml** + +````html +@page +@model MyProject.Web.Pages.IssuesModel +

Issue List

+ + + + + + + + + @foreach (var issue in Model.Issues) + { + + + + + } + +
IssueClosed?
@issue.Title + @if (issue.IsClosed) + { + Closed + } + else + { + Open + } +
+```` + +本页仅创建一个包含issue的表格: + +![issue-list](../../images/issue-list.png) + +你可以在`.Web.Tests`项目中编写一个测试类如下所示: + +````csharp +using System.Threading.Tasks; +using HtmlAgilityPack; +using Shouldly; +using Xunit; + +namespace MyProject.Pages +{ + public class Issues_Tests : MyProjectWebTestBase + { + [Fact] + public async Task Should_Get_Table_Of_Issues() + { + // Act + + var response = await GetResponseAsStringAsync("/Issues"); + + //Assert + + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(response); + + var tableElement = htmlDocument.GetElementbyId("IssueTable"); + tableElement.ShouldNotBeNull(); + + var trNodes = tableElement.SelectNodes("//tbody/tr"); + trNodes.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +`GetResponseAsStringAsync`是一个快捷方法, 它来自执行HTTP GET请求的基类, 检查生成的HTTP状态是否为`200`, 并将响应作为`string`返回. + +> 你可以使用`Client`对象(类型为`HttpClient`)对服务器执行任何类型的请求, 并读取响应.`GetResponseAsStringAsync`只是一种快捷方法. + +本例使用[HtmlAgilityPack](https://html-agility-pack.net/)库来解析传入的HTML并测试它是否包含issue表格. + +> 本例假设的数据库中存在一些初始issue. 请参阅[测试文档](../../Testing.md)的*种子数据*部分, 了解如何设置种子数据, 以便可以假定数据库中有一些可用的初始数据. + +## 控制器测试 + +测试控制器也不例外. 只需使用正确的URL向服务器执行请求, 获取响应并做出断言. + +### 查看结果 + +如果控制器返回一个视图, 你可以使用类似的代码来测试返回的HTML. 参见上面的Razor页面示例. + +### 对象结果 + +如果控制器返回对象结果, 则可以使用`GetResponseAsObjectAsync`方法. + +假设你有一个如下定义的控制器: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using MyProject.Issues; +using Volo.Abp.AspNetCore.Mvc; + +namespace MyProject.Web.Controllers +{ + [Route("api/issues")] + public class IssueController : AbpController + { + private readonly IIssueAppService _issueAppService; + + public IssueController(IIssueAppService issueAppService) + { + _issueAppService = issueAppService; + } + + [HttpGet] + public async Task> GetAsync() + { + return await _issueAppService.GetListAsync(); + } + } +} +```` + +你可以编写测试代码来调用API并获得结果: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using MyProject.Issues; +using Shouldly; +using Xunit; + +namespace MyProject.Pages +{ + public class Issues_Tests : MyProjectWebTestBase + { + [Fact] + public async Task Should_Get_Issues_From_Api() + { + var issues = await GetResponseAsObjectAsync>("/api/issues"); + + issues.ShouldNotBeNull(); + issues.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +## 测试JavaScript代码 + +ABP框架不提供任何基础设施来测试JavaScript代码. 你可以使用任何测试框架和工具来测试JavaScript代码. + +## 测试基础设施 + +[Volo.Abp.AspNetCore.TestBase](https://www.nuget.org/packages/Volo.Abp.AspNetCore.TestBase) 提供了集成到ABP框架和ASP.NET Core的测试基础设施. + +> Volo.Abp.AspNetCore.TestBase 已经安装在 `.Web.Tests` 项目中. + +此包提供的`AbpAspNetCoreIntegratedTestBase`作为派生测试类的基类. 上面使用的`MyProjectWebTestBase`继承自`AbpAspNetCoreIntegratedTestBase`, 因此我们间接继承了`AbpAspNetCoreIntegratedTestBase`. + +### 基本属性 + +`AbpAspNetCoreIntegratedTestBase` 提供了测试中使用的以下基本属性: + +* `Server`: 在测试中托管web应用程序的`TestServer`实例. +* `Client`: 为执行对测试服务器的请求配置`HttpClient`实例. +* `ServiceProvider`: 可以在你需要时处理服务提供服务. + +### 基本方法 + +`AbpAspNetCoreIntegratedTestBase` 提供了以下方法, 如果需要自定义测试服务器, 可以重写这些方法: + +* `ConfigureServices` 仅为派生测试类注册/替换服务时可以重写使用. +* `CreateHostBuilder` 可用于自定义生成 `IHostBuilder`. + +另请参阅 + +* [总览/服务器端测试](../../Testing.md) diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index db0fc10aed..d66d11b3cf 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -262,6 +262,10 @@ "text": "种子数据", "path": "Data-Seeding.md" }, + { + "text": "分布式锁", + "path": "Distributed-Locking.md" + }, { "text": "虚拟文件系统", "path": "Virtual-File-System.md" @@ -405,14 +409,16 @@ "path": "Entities.md" }, { - "text": "值对象" + "text": "值对象", + "path": "Value-Objects.md" }, { "text": "仓储", "path": "Repositories.md" }, { - "text": "领域服务" + "text": "领域服务", + "path": "Domain-Services.md" }, { "text": "规约", @@ -515,6 +521,10 @@ "text": "自定义/扩展UI", "path": "UI/AspNetCore/Customization-User-Interface.md" }, + { + "text": "测试", + "path": "UI/AspNetCore/Testing.md" + }, { "text": "主题化", "path": "UI/AspNetCore/Theming.md" @@ -536,6 +546,10 @@ "text": "服务代理", "path": "UI/Angular/Service-Proxies.md" }, + { + "text": "单元测试", + "path": "UI/Angular/Testing.md" + }, { "text": "HTTP请求", "path": "UI/Angular/HTTP-Requests.md" @@ -682,6 +696,20 @@ } ] }, + { + "text": "测试", + "path": "Testing.md" + }, + { + "text": "部署", + "path": "Deployment/Index.md", + "items": [ + { + "text": "部署到群集环境", + "path": "Deployment/Clustered-Environment.md" + } + ] + }, { "text": "示例", "items": [ @@ -782,7 +810,13 @@ "items": [ { "text": "CLI", - "path": "CLI.md" + "path": "CLI.md", + "items": [ + { + "text": "新命令示例", + "path": "CLI-New-Command-Samples.md" + } + ] }, { "text": "API文档", diff --git a/docs/zh-Hans/images/deployment-clustered.png b/docs/zh-Hans/images/deployment-clustered.png new file mode 100644 index 0000000000..74177482c5 Binary files /dev/null and b/docs/zh-Hans/images/deployment-clustered.png differ diff --git a/docs/zh-Hans/images/deployment-single-instance.png b/docs/zh-Hans/images/deployment-single-instance.png new file mode 100644 index 0000000000..8c3f8719b2 Binary files /dev/null and b/docs/zh-Hans/images/deployment-single-instance.png differ diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index f3e495c814..94a2e79415 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -403,6 +403,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.DistributedLocking EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BackgroundWorkers.Hangfire", "src\Volo.Abp.BackgroundWorkers.Hangfire\Volo.Abp.BackgroundWorkers.Hangfire.csproj", "{E5FCE710-C5A3-4F94-B9C9-BD1E99252BFB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Gdpr.Abstractions", "src\Volo.Abp.Gdpr.Abstractions\Volo.Abp.Gdpr.Abstractions.csproj", "{3683340D-92F5-4B14-B77B-34A163333309}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1201,6 +1203,10 @@ Global {E5FCE710-C5A3-4F94-B9C9-BD1E99252BFB}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5FCE710-C5A3-4F94-B9C9-BD1E99252BFB}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5FCE710-C5A3-4F94-B9C9-BD1E99252BFB}.Release|Any CPU.Build.0 = Release|Any CPU + {3683340D-92F5-4B14-B77B-34A163333309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3683340D-92F5-4B14-B77B-34A163333309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3683340D-92F5-4B14-B77B-34A163333309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3683340D-92F5-4B14-B77B-34A163333309}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1404,6 +1410,7 @@ Global {CA805B77-D50C-431F-B3CB-1111C9C6E807} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {C4F54FB5-C828-414D-BA03-E8E7A10C784D} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E5FCE710-C5A3-4F94-B9C9-BD1E99252BFB} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {3683340D-92F5-4B14-B77B-34A163333309} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index bf370840a9..92bfc9ddb3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -268,7 +268,6 @@ public class AbpInputTagHelperService : AbpTagHelperService } var label = new TagBuilder("label"); - label.AddCssClass("form-label"); label.Attributes.Add("for", GetIdAttributeValue(inputTag)); label.InnerHtml.AppendHtml(TagHelper.Label); @@ -291,15 +290,10 @@ public class AbpInputTagHelperService : AbpTagHelperService { if (IsOutputHidden(inputTag)) { - return ""; - } - - if (isCheckbox) - { - return ""; + return string.Empty; } - var text = ""; + string text; if (!string.IsNullOrEmpty(TagHelper.InfoText)) { @@ -314,7 +308,7 @@ public class AbpInputTagHelperService : AbpTagHelperService } else { - return ""; + return string.Empty; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json index 25516ba765..f3259dbb54 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json @@ -1,7 +1,7 @@ { "culture": "en", "texts": { - "GivenTenantIsNotExist": "Given tenant is not exist: {0}", + "GivenTenantIsNotExist": "Given tenant does not exist: {0}", "GivenTenantIsNotAvailable": "Given tenant is not available: {0}", "Tenant": "Tenant", "Switch": "switch", diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs index 35bcb71d53..9a5eecb4f6 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/ModuleProjectBuildPipelineBuilder.cs @@ -14,6 +14,13 @@ public static class ModuleProjectBuildPipelineBuilder pipeline.Steps.Add(new ReplaceCommonPropsStep()); pipeline.Steps.Add(new MakeProxyJsonFileEmbeddedStep()); pipeline.Steps.Add(new ReplaceConfigureAwaitPropsStep()); + + if (context.Module.IsPro) + { + pipeline.Steps.Add(new CreateAppSettingsSecretsStep()); + pipeline.Steps.Add(new LicenseCodeReplaceStep()); + } + pipeline.Steps.Add(new UpdateNuGetConfigStep("/NuGet.Config")); pipeline.Steps.Add(new CreateProjectResultZipStep()); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/CreateAppSettingsSecretsStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/CreateAppSettingsSecretsStep.cs index 5a84bda994..20e4f8fca6 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/CreateAppSettingsSecretsStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/CreateAppSettingsSecretsStep.cs @@ -30,7 +30,9 @@ public class CreateAppSettingsSecretsStep : ProjectBuildPipelineStep ); } - var projectFiles = context.Files.Where(x => x.Content.Contains(AppSettingsPlaceholder)).ToList(); + var projectFiles = context.Files.Where(x => + x.Name.EndsWith(".csproj") && + x.Content.Contains(AppSettingsPlaceholder)).ToList(); foreach (var projectFile in projectFiles) { @@ -40,7 +42,21 @@ public class CreateAppSettingsSecretsStep : ProjectBuildPipelineStep private static byte[] GetAppSettingsSecretJsonContent(ProjectBuildContext context) { - return context.Template.IsPro() + bool condition; + if (context.Template != null) + { + condition = context.Template.IsPro(); + } + else if (context.Module != null) + { + condition = context.Module.IsPro; + } + else + { + condition = false; + } + + return condition ? $"{{{Environment.NewLine} \"AbpLicenseCode\": \"{CliConsts.LicenseCodePlaceHolder}\" {Environment.NewLine}}}".GetBytes() : $"{{{Environment.NewLine}}}".GetBytes(); } @@ -52,11 +68,30 @@ public class CreateAppSettingsSecretsStep : ProjectBuildPipelineStep private static string ReplaceAppSettingsSecretsPlaceholder(string content) { - var replaceContent = $"{Environment.NewLine}" + - $" {Environment.NewLine}" + + var path = string.Empty; + + var appSettingsRemoveLine = content.SplitToLines().FirstOrDefault(l=> + l.Contains("None") && + l.Contains("Remove") && + l.Contains(CliConsts.AppSettingsJsonFileName)); + + if (appSettingsRemoveLine != null) + { + var prefix = appSettingsRemoveLine.Split("\"") + .Select(s => s.Trim()) + .First(s => s.Contains(CliConsts.AppSettingsJsonFileName)) + .Replace(CliConsts.AppSettingsJsonFileName, ""); + + path = string.IsNullOrWhiteSpace(prefix) ? string.Empty : prefix; + } + + + var replaceContent = $"{Environment.NewLine}" + + $" {Environment.NewLine}" + $" PreserveNewest{Environment.NewLine}" + $" Always{Environment.NewLine}" + " "; + return content.Replace(AppSettingsPlaceholder, replaceContent); } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MoveFileStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MoveFileStep.cs new file mode 100644 index 0000000000..4c36197f76 --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MoveFileStep.cs @@ -0,0 +1,28 @@ +using System.Linq; + +namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps; + +public class MoveFileStep : ProjectBuildPipelineStep +{ + private readonly string _filePath; + private readonly string _newPath; + + public MoveFileStep(string filePath, string newPath) + { + _filePath = filePath; + _newPath = newPath; + } + + public override void Execute(ProjectBuildContext context) + { + var fileToMove = context.Files.Find(x => x.Name == _filePath); + var newFileExist = context.Files.Any(x => x.Name == _newPath); + + if (fileToMove == null || newFileExist) + { + return; + } + + fileToMove.SetName(_newPath); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs index 203a927536..8e66d26547 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs @@ -12,7 +12,7 @@ public class RemoveFileStep : ProjectBuildPipelineStep public override void Execute(ProjectBuildContext context) { - var fileToRemove = context.Files.Find(x => x.Name.EndsWith(_filePath)); + var fileToRemove = context.Files.Find(x => x.Name == _filePath);; if (fileToRemove != null) { context.Files.Remove(fileToRemove); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs index 39186e900c..c7a30ab2e4 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs @@ -12,6 +12,9 @@ public class TemplateCodeDeleteStep : ProjectBuildPipelineStep file.Name.EndsWith(".csproj") || file.Name.EndsWith(".cshtml") || file.Name.EndsWith(".json") || + file.Name.EndsWith(".gitignore") || + file.Name.EndsWith(".yml") || + file.Name.EndsWith(".ps1") || file.Name.EndsWith(".html")) { file.RemoveTemplateCode(context.Symbols); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs index 312181e721..e151646988 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppNoLayersTemplateBase.cs @@ -81,19 +81,12 @@ public abstract class AppNoLayersTemplateBase : AppTemplateBase break; } - //ConfigureTenantSchema(context, steps); - //SwitchDatabaseProvider(context, steps); - //DeleteUnrelatedProjects(context, steps); steps.Add(new RemoveFolderStep("/aspnet-core/MyCompanyName.MyProjectName/Migrations")); - //RemoveMigrations(context, steps); - //ConfigureTieredArchitecture(context, steps); - //ConfigurePublicWebSite(context, steps); - //RemoveUnnecessaryPorts(context, steps); RandomizeSslPorts(context, steps); RandomizeStringEncryption(context, steps); UpdateNuGetConfig(context, steps); ChangeConnectionString(context, steps); - //CleanupFolderHierarchy(context, steps); + ConfigureDockerFiles(context, steps); if (context.BuildArgs.UiFramework != UiFramework.Angular) { @@ -102,4 +95,35 @@ public abstract class AppNoLayersTemplateBase : AppTemplateBase return steps; } + + protected void ConfigureDockerFiles(ProjectBuildContext context, List steps) + { + switch (context.BuildArgs.UiFramework) + { + case UiFramework.None: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.Server.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Host.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.Angular: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.Server.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Host.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.BlazorServer: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Host.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.Server.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.NotSpecified: + case UiFramework.Mvc: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.Server.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Host.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + } + } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs index 3dd6ece476..6a0b532135 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs @@ -466,4 +466,36 @@ public abstract class AppTemplateBase : TemplateInfo steps.Add(new MoveFolderStep("/aspnet-core/", "/")); } } + + private void ConfigureDockerFiles(ProjectBuildContext context, List steps) + { + switch (context.BuildArgs.UiFramework) + { + case UiFramework.None: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Angular.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.Angular: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Angular.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.Blazor: + case UiFramework.BlazorServer: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Angular.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + case UiFramework.NotSpecified: + case UiFramework.Mvc: + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Blazor.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/docker-compose.Angular.yml")); + steps.Add(new RemoveFileStep("/aspnet-core/etc/docker/dynamic-env.json")); + steps.Add(new MoveFileStep("/aspnet-core/etc/docker/docker-compose.Mvc.yml", "/aspnet-core/etc/docker/docker-compose.yml")); + break; + } + } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs index e6069981da..7e30a9a6bf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/Angular/AngularServiceProxyGenerator.cs @@ -48,7 +48,7 @@ public class AngularServiceProxyGenerator : ServiceProxyGeneratorBase : ExtensibleCreati /// public Guid? LastModifierId { get; set; } + + protected ExtensibleAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -30,4 +43,16 @@ public abstract class ExtensibleAuditedEntityDto : ExtensibleCreationAuditedEnti /// public Guid? LastModifierId { get; set; } + + protected ExtensibleAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs index 05193efc4a..5f6fd28628 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleAuditedEntityWithUserDto.cs @@ -1,5 +1,6 @@ using System; using Volo.Abp.Auditing; +using Volo.Abp.Data; namespace Volo.Abp.Application.Dtos; @@ -18,6 +19,18 @@ public abstract class ExtensibleAuditedEntityWithUserDto /// public TUserDto LastModifier { get; set; } + + protected ExtensibleAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -35,4 +48,16 @@ public abstract class ExtensibleAuditedEntityWithUserDto : ExtensibleA /// public TUserDto LastModifier { get; set; } + + protected ExtensibleAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs index ba3bec655d..9230759052 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityDto.cs @@ -1,5 +1,6 @@ using System; using Volo.Abp.Auditing; +using Volo.Abp.Data; namespace Volo.Abp.Application.Dtos; @@ -16,6 +17,18 @@ public abstract class ExtensibleCreationAuditedEntityDto : Extensib /// public Guid? CreatorId { get; set; } + + protected ExtensibleCreationAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleCreationAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -30,4 +43,16 @@ public abstract class ExtensibleCreationAuditedEntityDto : ExtensibleEntityDto, /// public Guid? CreatorId { get; set; } + + protected ExtensibleCreationAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleCreationAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs index e6a31b4a68..3ea1be97ab 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleCreationAuditedEntityWithUserDto.cs @@ -1,5 +1,6 @@ using System; using Volo.Abp.Auditing; +using Volo.Abp.Data; namespace Volo.Abp.Application.Dtos; @@ -14,6 +15,18 @@ namespace Volo.Abp.Application.Dtos; public abstract class ExtensibleCreationAuditedEntityWithUserDto : ExtensibleCreationAuditedEntityDto, ICreationAuditedObject { public TUserDto Creator { get; set; } + + protected ExtensibleCreationAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleCreationAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -27,4 +40,16 @@ public abstract class ExtensibleCreationAuditedEntityWithUserDto : Ext ICreationAuditedObject { public TUserDto Creator { get; set; } + + protected ExtensibleCreationAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleCreationAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs index 788825e842..1af46a3287 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs @@ -11,6 +11,18 @@ public abstract class ExtensibleEntityDto : ExtensibleObject, IEntityDto public TKey Id { get; set; } + protected ExtensibleEntityDto() + : this(true) + { + + } + + protected ExtensibleEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } + public override string ToString() { return $"[DTO: {GetType().Name}] Id = {Id}"; @@ -20,6 +32,18 @@ public abstract class ExtensibleEntityDto : ExtensibleObject, IEntityDto : ExtensibleAu /// public DateTime? DeletionTime { get; set; } + + protected ExtensibleFullAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleFullAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -36,4 +49,16 @@ public abstract class ExtensibleFullAuditedEntityDto : ExtensibleAuditedEntityDt /// public DateTime? DeletionTime { get; set; } + + protected ExtensibleFullAuditedEntityDto() + : this(true) + { + + } + + protected ExtensibleFullAuditedEntityDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs index bd1ee89bcb..018a84cd7a 100644 --- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs +++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleFullAuditedEntityWithUserDto.cs @@ -1,5 +1,6 @@ using System; using Volo.Abp.Auditing; +using Volo.Abp.Data; namespace Volo.Abp.Application.Dtos; @@ -21,6 +22,18 @@ public abstract class ExtensibleFullAuditedEntityWithUserDto public TUserDto Deleter { get; set; } + + protected ExtensibleFullAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleFullAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } /// @@ -41,4 +54,16 @@ public abstract class ExtensibleFullAuditedEntityWithUserDto : Extensi /// public TUserDto Deleter { get; set; } + + protected ExtensibleFullAuditedEntityWithUserDto() + : this(true) + { + + } + + protected ExtensibleFullAuditedEntityWithUserDto(bool setDefaultsForExtraProperties) + : base(setDefaultsForExtraProperties) + { + + } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/DistributedEvents/PostgreSqlDbContextEventOutbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/DistributedEvents/PostgreSqlDbContextEventOutbox.cs index e5f2e0c4b6..7c5ef12fa7 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/DistributedEvents/PostgreSqlDbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/DistributedEvents/PostgreSqlDbContextEventOutbox.cs @@ -13,7 +13,7 @@ public class PostgreSqlDbContextEventOutbox : DbContextEventOutbox : IDbContextEventOutbox ids) + { + var dbContext = (IHasEventOutbox)await DbContextProvider.GetDbContextAsync(); + var outgoingEvents = await dbContext.OutgoingEvents.Where(x => ids.Contains(x.Id)).ToListAsync(); + if (outgoingEvents.Any()) + { + dbContext.RemoveRange(outgoingEvents); + } + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IDistributedEventHandler.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventHandler.cs similarity index 100% rename from framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IDistributedEventHandler.cs rename to framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventHandler.cs diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventHandler.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventHandler.cs similarity index 83% rename from framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventHandler.cs rename to framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventHandler.cs index bbb35b8124..e313545b81 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventHandler.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventHandler.cs @@ -3,7 +3,7 @@ using Volo.Abp.EventBus.Distributed; namespace Volo.Abp.EventBus; /// -/// Undirect base interface for all event handlers. +/// Indirect base interface for all event handlers. /// Implement or instead of this one. /// public interface IEventHandler diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventHandler.cs similarity index 81% rename from framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs rename to framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventHandler.cs index 94ebb31188..76f2053411 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/ILocalEventHandler.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventHandler.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; +// ReSharper disable once CheckNamespace (Keeping for backward compability) namespace Volo.Abp.EventBus; -//TODO: Move to the right namespace in v3.0 public interface ILocalEventHandler : IEventHandler { /// diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index d8a3d1341a..86500cf434 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -71,6 +71,10 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen private async Task ProcessEventAsync(ServiceBusReceivedMessage message) { var eventName = message.Subject; + if (eventName == null) + { + return; + } var eventType = _eventTypes.GetOrDefault(eventName); if (eventType == null) { @@ -87,12 +91,33 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen await TriggerHandlersAsync(eventType, eventData); } - public override async Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) + public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData, outgoingEvent.Id); } - public override async Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) + public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) + { + var outgoingEventArray = outgoingEvents.ToArray(); + + var publisher = await _publisherPool.GetAsync( + _options.TopicName, + _options.ConnectionName); + + using var messageBatch = await publisher.CreateMessageBatchAsync(); + + foreach (var outgoingEvent in outgoingEventArray) + { + if (!messageBatch.TryAddMessage(new ServiceBusMessage(outgoingEvent.EventData) { Subject = outgoingEvent.EventName })) + { + throw new AbpException("The message is too large to fit in the batch. Set AbpEventBusBoxesOptions.OutboxWaitingEventMaxCount to reduce the number"); + } + } + + await publisher.SendMessagesAsync(messageBatch); + } + + public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { var eventType = _eventTypes.GetOrDefault(incomingEvent.EventName); if (eventType == null) @@ -180,7 +205,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen .Locking(factories => factories.Clear()); } - protected override async Task PublishToEventBusAsync(Type eventType, object eventData) + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData); } diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index a19b01311c..4dba728c51 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -79,12 +79,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return; } - string messageId = null; - - if (message.Headers.TryGetLastBytes("messageId", out var messageIdBytes)) - { - messageId = System.Text.Encoding.UTF8.GetString(messageIdBytes); - } + var messageId = message.GetMessageId(); if (await AddToInboxAsync(messageId, eventName, eventType, message.Value)) { @@ -164,7 +159,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } - protected override async Task PublishToEventBusAsync(Type eventType, object eventData) + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync( eventType, @@ -198,7 +193,43 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen ); } - public override async Task ProcessFromInboxAsync( + public override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) + { + var producer = ProducerPool.Get(); + var outgoingEventArray = outgoingEvents.ToArray(); + producer.BeginTransaction(); + try + { + foreach (var outgoingEvent in outgoingEventArray) + { + var messageId = outgoingEvent.Id.ToString("N"); + var headers = new Headers + { + { "messageId", System.Text.Encoding.UTF8.GetBytes(messageId)} + }; + + producer.Produce( + AbpKafkaEventBusOptions.TopicName, + new Message + { + Key = outgoingEvent.EventName, + Value = outgoingEvent.EventData, + Headers = headers + }); + } + + producer.CommitTransaction(); + } + catch (Exception e) + { + producer.AbortTransaction(); + throw; + } + + return Task.CompletedTask; + } + + public async override Task ProcessFromInboxAsync( IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { @@ -241,13 +272,29 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishAsync(topicName, eventName, body, headers, headersArguments); } - private async Task PublishAsync(string topicName, string eventName, byte[] body, Headers headers, Dictionary headersArguments) + private Task> PublishAsync( + string topicName, + string eventName, + byte[] body, + Headers headers, + Dictionary headersArguments) { var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName); + return PublishAsync(producer, topicName, eventName, body, headers, headersArguments); + } + + private Task> PublishAsync( + IProducer producer, + string topicName, + string eventName, + byte[] body, + Headers headers, + Dictionary headersArguments) + { SetEventMessageHeaders(headers, headersArguments); - await producer.ProduceAsync( + return producer.ProduceAsync( topicName, new Message { diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/MessageExtensions.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/MessageExtensions.cs new file mode 100644 index 0000000000..17a80ec87c --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/MessageExtensions.cs @@ -0,0 +1,18 @@ +using Confluent.Kafka; + +namespace Volo.Abp.EventBus.Kafka; + +public static class MessageExtensions +{ + public static string GetMessageId(this Message message) + { + string messageId = null; + + if (message.Headers.TryGetLastBytes("messageId", out var messageIdBytes)) + { + messageId = System.Text.Encoding.UTF8.GetString(messageIdBytes); + } + + return messageId; + } +} diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index a2932de52e..c9ec49bc7e 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -34,6 +34,8 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; } protected IRabbitMqMessageConsumer Consumer { get; private set; } + private bool _exchangeCreated; + public RabbitMqDistributedEventBus( IOptions options, IConnectionPool connectionPool, @@ -180,7 +182,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } - protected override async Task PublishToEventBusAsync(Type eventType, object eventData) + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(eventType, eventData, null); } @@ -197,7 +199,30 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe return PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData, null, eventId: outgoingEvent.Id); } - public override async Task ProcessFromInboxAsync( + public async override Task PublishManyFromOutboxAsync( + IEnumerable outgoingEvents, + OutboxConfig outboxConfig) + { + using (var channel = ConnectionPool.Get(AbpRabbitMqEventBusOptions.ConnectionName).CreateModel()) + { + var outgoingEventArray = outgoingEvents.ToArray(); + channel.ConfirmSelect(); + + foreach (var outgoingEvent in outgoingEventArray) + { + await PublishAsync( + channel, + outgoingEvent.EventName, + outgoingEvent.EventData, + properties: null, + eventId: outgoingEvent.Id); + } + + channel.WaitForConfirmsOrDie(); + } + } + + public async override Task ProcessFromInboxAsync( IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { @@ -233,7 +258,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe return PublishAsync(eventName, body, properties, headersArguments); } - protected Task PublishAsync( + protected virtual Task PublishAsync( string eventName, byte[] body, IBasicProperties properties, @@ -242,37 +267,66 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe { using (var channel = ConnectionPool.Get(AbpRabbitMqEventBusOptions.ConnectionName).CreateModel()) { - channel.ExchangeDeclare( - AbpRabbitMqEventBusOptions.ExchangeName, - AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(), - durable: true - ); - - if (properties == null) - { - properties = channel.CreateBasicProperties(); - properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent; - } + return PublishAsync(channel, eventName, body, properties, headersArguments, eventId); + } + } - if (properties.MessageId.IsNullOrEmpty()) - { - properties.MessageId = (eventId ?? GuidGenerator.Create()).ToString("N"); - } + protected virtual Task PublishAsync( + IModel channel, + string eventName, + byte[] body, + IBasicProperties properties, + Dictionary headersArguments = null, + Guid? eventId = null) + { + EnsureExchangeExists(channel); - SetEventMessageHeaders(properties, headersArguments); + if (properties == null) + { + properties = channel.CreateBasicProperties(); + properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent; + } - channel.BasicPublish( - exchange: AbpRabbitMqEventBusOptions.ExchangeName, - routingKey: eventName, - mandatory: true, - basicProperties: properties, - body: body - ); + if (properties.MessageId.IsNullOrEmpty()) + { + properties.MessageId = (eventId ?? GuidGenerator.Create()).ToString("N"); } + SetEventMessageHeaders(properties, headersArguments); + + channel.BasicPublish( + exchange: AbpRabbitMqEventBusOptions.ExchangeName, + routingKey: eventName, + mandatory: true, + basicProperties: properties, + body: body + ); + return Task.CompletedTask; } + private void EnsureExchangeExists(IModel channel) + { + if (_exchangeCreated) + { + return; + } + + try + { + channel.ExchangeDeclarePassive(AbpRabbitMqEventBusOptions.ExchangeName); + } + catch (Exception) + { + channel.ExchangeDeclare( + AbpRabbitMqEventBusOptions.ExchangeName, + AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(), + durable: true + ); + } + _exchangeCreated = true; + } + private void SetEventMessageHeaders(IBasicProperties properties, Dictionary headersArguments) { if (headersArguments == null) @@ -306,7 +360,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe var handlerFactoryList = new List(); foreach (var handlerFactory in - HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { handlerFactoryList.Add( new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index cf6c1d6b0f..4ccd72ba15 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Rebus.Bus; using Rebus.Pipeline; +using Rebus.Transport; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; using Volo.Abp.Guids; @@ -149,6 +150,11 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) + { + await PublishAsync(eventType, eventData); + } + + protected virtual async Task PublishAsync(Type eventType, object eventData) { if (AbpRebusEventBusOptions.Publish != null) { @@ -218,7 +224,22 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishToEventBusAsync(eventType, eventData); } - public override async Task ProcessFromInboxAsync( + public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) + { + var outgoingEventArray = outgoingEvents.ToArray(); + + using (var scope = new RebusTransactionScope()) + { + foreach (var outgoingEvent in outgoingEventArray) + { + await PublishFromOutboxAsync(outgoingEvent, outboxConfig); + } + + await scope.CompleteAsync(); + } + } + + public async override Task ProcessFromInboxAsync( IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs index a46fc76238..d92c48db3a 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs @@ -34,6 +34,11 @@ public class AbpEventBusBoxesOptions /// Default: 2 hours /// public TimeSpan WaitTimeToDeleteProcessedInboxEvents { get; set; } + + /// + /// Default: true + /// + public bool BatchPublishOutboxEvents { get; set; } public AbpEventBusBoxesOptions() { @@ -43,5 +48,6 @@ public class AbpEventBusBoxesOptions PeriodTimeSpan = TimeSpan.FromSeconds(2); DistributedLockWaitDuration = TimeSpan.FromSeconds(15); WaitTimeToDeleteProcessedInboxEvents = TimeSpan.FromHours(2); + BatchPublishOutboxEvents = true; } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 6547c1686c..69ae91505d 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -85,6 +85,11 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB OutboxConfig outboxConfig ); + public abstract Task PublishManyFromOutboxAsync( + IEnumerable outgoingEvents, + OutboxConfig outboxConfig + ); + public abstract Task ProcessFromInboxAsync( IncomingEventInfo incomingEvent, InboxConfig inboxConfig); @@ -101,7 +106,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { - var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); + var eventOutbox = + (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); var eventName = EventNameAttribute.GetNameOrDefault(eventType); await eventOutbox.EnqueueAsync( new OutgoingEventInfo( @@ -135,7 +141,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (inboxConfig.EventSelector == null || inboxConfig.EventSelector(eventType)) { - var eventInbox = (IEventInbox)scope.ServiceProvider.GetRequiredService(inboxConfig.ImplementationType); + var eventInbox = + (IEventInbox)scope.ServiceProvider.GetRequiredService(inboxConfig.ImplementationType); if (!messageId.IsNullOrEmpty()) { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IEventOutbox.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IEventOutbox.cs index 6ecefcf002..018747945c 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IEventOutbox.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/IEventOutbox.cs @@ -12,4 +12,6 @@ public interface IEventOutbox Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default); Task DeleteAsync(Guid id); + + Task DeleteManyAsync(IEnumerable ids); } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/ISupportsEventBoxes.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/ISupportsEventBoxes.cs index 37405fb58a..73f74a68ad 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/ISupportsEventBoxes.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/ISupportsEventBoxes.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Volo.Abp.EventBus.Distributed; @@ -9,6 +10,11 @@ public interface ISupportsEventBoxes OutboxConfig outboxConfig ); + Task PublishManyFromOutboxAsync( + IEnumerable outgoingEvents, + OutboxConfig outboxConfig + ); + Task ProcessFromInboxAsync( IncomingEventInfo incomingEvent, InboxConfig inboxConfig diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 1eb7689aba..64f375130b 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Options; using Volo.Abp.Collections; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Local; -using Volo.Abp.Uow; namespace Volo.Abp.EventBus.Distributed; @@ -16,19 +15,16 @@ public class LocalDistributedEventBus : IDistributedEventBus, ISingletonDependen { private readonly ILocalEventBus _localEventBus; - protected IUnitOfWorkManager UnitOfWorkManager { get; } protected IServiceScopeFactory ServiceScopeFactory { get; } protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; } public LocalDistributedEventBus( ILocalEventBus localEventBus, - IUnitOfWorkManager unitOfWorkManager, IServiceScopeFactory serviceScopeFactory, IOptions distributedEventBusOptions) { _localEventBus = localEventBus; - UnitOfWorkManager = unitOfWorkManager; ServiceScopeFactory = serviceScopeFactory; AbpDistributedEventBusOptions = distributedEventBusOptions.Value; Subscribe(distributedEventBusOptions.Value.Handlers); @@ -126,56 +122,24 @@ public class LocalDistributedEventBus : IDistributedEventBus, ISingletonDependen _localEventBus.UnsubscribeAll(eventType); } - public async Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) + public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class { - await PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete); + return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); } - public async Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) + public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) { - if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) - { - AddToUnitOfWork( - UnitOfWorkManager.Current, - new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext()) - ); - return; - } - - await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); - } - - public async Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) - where TEvent : class - { - await PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete, useOutbox); + return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); } - - public async Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + + public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class { - if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) - { - AddToUnitOfWork( - UnitOfWorkManager.Current, - new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext(), useOutbox) - ); - return; - } - - if (useOutbox && UnitOfWorkManager.Current != null) - { - UnitOfWorkManager.Current.OnCompleted(async() => { - await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); - }); - return; - } - - await _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete: false); + return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete); } - protected virtual void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) + public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { - unitOfWork.AddOrReplaceDistributedEvent(eventRecord); + return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete); } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs index 6ed60ba34c..851ddf3da1 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -82,18 +85,14 @@ public class OutboxSender : IOutboxSender, ITransientDependency } Logger.LogInformation($"Found {waitingEvents.Count} events in the outbox."); - - foreach (var waitingEvent in waitingEvents) + + if (EventBusBoxesOptions.BatchPublishOutboxEvents) { - await DistributedEventBus - .AsSupportsEventBoxes() - .PublishFromOutboxAsync( - waitingEvent, - OutboxConfig - ); - - await Outbox.DeleteAsync(waitingEvent.Id); - Logger.LogInformation($"Sent the event to the message broker with id = {waitingEvent.Id:N}"); + await PublishOutgoingMessagesInBatchAsync(waitingEvents); + } + else + { + await PublishOutgoingMessagesAsync(waitingEvents); } } } @@ -108,4 +107,32 @@ public class OutboxSender : IOutboxSender, ITransientDependency } } } + + protected virtual async Task PublishOutgoingMessagesAsync(List waitingEvents) + { + foreach (var waitingEvent in waitingEvents) + { + await DistributedEventBus + .AsSupportsEventBoxes() + .PublishFromOutboxAsync( + waitingEvent, + OutboxConfig + ); + + await Outbox.DeleteAsync(waitingEvent.Id); + + Logger.LogInformation($"Sent the event to the message broker with id = {waitingEvent.Id:N}"); + } + } + + protected virtual async Task PublishOutgoingMessagesInBatchAsync(List waitingEvents) + { + await DistributedEventBus + .AsSupportsEventBoxes() + .PublishManyFromOutboxAsync(waitingEvents, OutboxConfig); + + await Outbox.DeleteManyAsync(waitingEvents.Select(x => x.Id).ToArray()); + + Logger.LogInformation($"Sent {waitingEvents.Count} events to message broker"); + } } diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xml b/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xml new file mode 100644 index 0000000000..bd6def337b --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xsd b/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj new file mode 100644 index 0000000000..8af8416bb0 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo.Abp.Gdpr.Abstractions.csproj @@ -0,0 +1,15 @@ + + + + + + + netstandard2.0 + + + + + + + + diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/AbpGdprAbstractionsModule.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/AbpGdprAbstractionsModule.cs new file mode 100644 index 0000000000..3cb53c8be2 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/AbpGdprAbstractionsModule.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.Gdpr; + +public class AbpGdprAbstractionsModule : AbpModule +{ +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprDataInfo.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprDataInfo.cs new file mode 100644 index 0000000000..550f556a1c --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprDataInfo.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Gdpr; + +[Serializable] +public class GdprDataInfo : Dictionary +{ + +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataDeletionRequestedEto.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataDeletionRequestedEto.cs new file mode 100644 index 0000000000..aa7b721117 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataDeletionRequestedEto.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.Gdpr; + +[Serializable] +public class GdprUserDataDeletionRequestedEto +{ + public Guid UserId { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataPreparedEto.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataPreparedEto.cs new file mode 100644 index 0000000000..3a2c440f97 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataPreparedEto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Volo.Abp.Gdpr; + +[Serializable] +public class GdprUserDataPreparedEto +{ + public Guid RequestId { get; set; } + + public string Provider { get; set; } + + public GdprDataInfo Data { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataProviderContext.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataProviderContext.cs new file mode 100644 index 0000000000..1ccd035b7e --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataProviderContext.cs @@ -0,0 +1,8 @@ +using System; + +namespace Volo.Abp.Gdpr; + +public class GdprUserDataProviderContext +{ + public Guid UserId { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataRequestedEto.cs b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataRequestedEto.cs new file mode 100644 index 0000000000..b341ab2659 --- /dev/null +++ b/framework/src/Volo.Abp.Gdpr.Abstractions/Volo/Abp/Gdpr/GdprUserDataRequestedEto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.Gdpr; + +[Serializable] +public class GdprUserDataRequestedEto +{ + public Guid UserId { get; set; } + + public Guid RequestId { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs index 81a5c195c1..7147c81271 100644 --- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs @@ -76,6 +76,7 @@ public class ConsumerPool : IConsumerPool, ISingletonDependency try { + consumer.Value.Unsubscribe(); consumer.Value.Close(); consumer.Value.Dispose(); } diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs index da21f48883..704c15845f 100644 --- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs @@ -80,9 +80,11 @@ public class KafkaMessageConsumer : IKafkaMessageConsumer, ITransientDependency, protected virtual async Task Timer_Elapsed(AbpAsyncTimer timer) { - await CreateTopicAsync(); - Consume(); - Timer.Stop(); + if (Consumer == null) + { + await CreateTopicAsync(); + Consume(); + } } protected virtual async Task CreateTopicAsync() @@ -164,12 +166,21 @@ public class KafkaMessageConsumer : IKafkaMessageConsumer, ITransientDependency, public virtual void Dispose() { + Timer.Stop(); if (Consumer == null) { return; } - Consumer.Close(); - Consumer.Dispose(); + try + { + Consumer.Unsubscribe(); + Consumer.Close(); + Consumer.Dispose(); + Consumer = null; + } + catch (ObjectDisposedException) + { + } } } diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs index 49287a20e9..0a31a3483a 100644 --- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs @@ -17,6 +17,8 @@ public class ProducerPool : IProducerPool, ISingletonDependency protected ConcurrentDictionary>> Producers { get; } protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10); + + protected TimeSpan DefaultTransactionsWaitDuration { get; set; } = TimeSpan.FromSeconds(30); public ILogger Logger { get; set; } @@ -37,11 +39,18 @@ public class ProducerPool : IProducerPool, ISingletonDependency return Producers.GetOrAdd( connectionName, connection => new Lazy>(() => { - var config = Options.Connections.GetOrDefault(connection); - - Options.ConfigureProducer?.Invoke(new ProducerConfig(config)); - - return new ProducerBuilder(config).Build(); + var producerConfig = new ProducerConfig(Options.Connections.GetOrDefault(connection)); + Options.ConfigureProducer?.Invoke(producerConfig); + + if (producerConfig.TransactionalId.IsNullOrWhiteSpace()) + { + producerConfig.TransactionalId = Guid.NewGuid().ToString(); + } + + var producer = new ProducerBuilder(producerConfig).Build(); + producer.InitTransactions(DefaultTransactionsWaitDuration); + + return producer; })).Value; } @@ -69,7 +78,7 @@ public class ProducerPool : IProducerPool, ISingletonDependency foreach (var producer in Producers.Values) { var poolItemDisposeStopwatch = Stopwatch.StartNew(); - + try { producer.Value.Dispose(); @@ -77,19 +86,19 @@ public class ProducerPool : IProducerPool, ISingletonDependency catch { } - + poolItemDisposeStopwatch.Stop(); - + remainingWaitDuration = remainingWaitDuration > poolItemDisposeStopwatch.Elapsed ? remainingWaitDuration.Subtract(poolItemDisposeStopwatch.Elapsed) : TimeSpan.Zero; } - + poolDisposeStopwatch.Stop(); - + Logger.LogInformation( $"Disposed Kafka Producer Pool ({Producers.Count} producers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms)."); - + if (poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0) { Logger.LogWarning( diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 0b88f72fe8..61621c2479 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -14,14 +14,17 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali public List BaseLocalizers { get; } + public AbpLocalizationOptions AbpLocalizationOptions { get; } + public virtual LocalizedString this[string name] => GetLocalizedString(name); public virtual LocalizedString this[string name, params object[] arguments] => GetLocalizedStringFormatted(name, arguments); - public AbpDictionaryBasedStringLocalizer(LocalizationResource resource, List baseLocalizers) + public AbpDictionaryBasedStringLocalizer(LocalizationResource resource, List baseLocalizers, AbpLocalizationOptions abpLocalizationOptions) { Resource = resource; BaseLocalizers = baseLocalizers; + AbpLocalizationOptions = abpLocalizationOptions; } public IEnumerable GetAllStrings(bool includeParentCultures) @@ -95,23 +98,29 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali return null; } - //Try to get from same language dictionary (without country code) - if (cultureName.Contains("-")) //Example: "tr-TR" (length=5) + if (AbpLocalizationOptions.TryToGetFromBaseCulture) { - var strLang = Resource.Contributors.GetOrNull(CultureHelper.GetBaseCultureName(cultureName), name); - if (strLang != null) + //Try to get from same language dictionary (without country code) + if (cultureName.Contains("-")) //Example: "tr-TR" (length=5) { - return strLang; + var strLang = Resource.Contributors.GetOrNull(CultureHelper.GetBaseCultureName(cultureName), name); + if (strLang != null) + { + return strLang; + } } } - //Try to get from default language - if (!Resource.DefaultCultureName.IsNullOrEmpty()) + if (AbpLocalizationOptions.TryToGetFromDefaultCulture) { - var strDefault = Resource.Contributors.GetOrNull(Resource.DefaultCultureName, name); - if (strDefault != null) + //Try to get from default language + if (!Resource.DefaultCultureName.IsNullOrEmpty()) { - return strDefault; + var strDefault = Resource.Contributors.GetOrNull(Resource.DefaultCultureName, name); + if (strDefault != null) + { + return strDefault; + } } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationOptions.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationOptions.cs index b2f289dfec..5228f886e9 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationOptions.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationOptions.cs @@ -21,6 +21,10 @@ public class AbpLocalizationOptions public Dictionary> LanguageFilesMap { get; } + public bool TryToGetFromBaseCulture { get; set; } + + public bool TryToGetFromDefaultCulture { get; set; } + public AbpLocalizationOptions() { Resources = new LocalizationResourceDictionary(); @@ -28,5 +32,7 @@ public class AbpLocalizationOptions Languages = new List(); LanguagesMap = new Dictionary>(); LanguageFilesMap = new Dictionary>(); + TryToGetFromBaseCulture = true; + TryToGetFromDefaultCulture = true; } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs index 8ee1eaefc4..3f47fd779d 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs @@ -68,7 +68,8 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return new StringLocalizerCacheItem( new AbpDictionaryBasedStringLocalizer( resource, - resource.BaseResourceTypes.Select(Create).ToList() + resource.BaseResourceTypes.Select(Create).ToList(), + AbpLocalizationOptions ) ); } diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs index 1f3ae999b0..cc6c4d42df 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs @@ -68,4 +68,18 @@ public class MongoDbContextEventOutbox : IMongoDbContextEventOu await dbContext.OutgoingEvents.DeleteOneAsync(x => x.Id.Equals(id)); } } + + [UnitOfWork] + public virtual async Task DeleteManyAsync(IEnumerable ids) + { + var dbContext = (IHasEventOutbox)await MongoDbContextProvider.GetDbContextAsync(); + if (dbContext.SessionHandle != null) + { + await dbContext.OutgoingEvents.DeleteManyAsync(dbContext.SessionHandle, x => ids.Contains(x.Id)); + } + else + { + await dbContext.OutgoingEvents.DeleteManyAsync(x => ids.Contains(x.Id)); + } + } } diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs index 049e8da712..8ce184295a 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqMessageConsumer.cs @@ -176,14 +176,6 @@ public class RabbitMqMessageConsumer : IRabbitMqMessageConsumer, ITransientDepen } catch (Exception ex) { - if (ex is OperationInterruptedException operationInterruptedException && - operationInterruptedException.ShutdownReason.ReplyCode == 406 && - operationInterruptedException.Message.Contains("arg 'x-dead-letter-exchange'")) - { - Logger.LogException(ex, LogLevel.Warning); - await ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning); - } - Logger.LogException(ex, LogLevel.Warning); await ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning); } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 497503680d..c239df38c9 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,9 +1,7 @@ using System; using System.Threading.Tasks; -using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.MultiTenancy; -using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.EventBus.Distributed; @@ -51,7 +49,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase public async Task Should_Get_TenantId_From_EventEto_Extra_Property() { var tenantId = Guid.NewGuid(); - + DistributedEventBus.Subscribe(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEto @@ -61,71 +59,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase {"TenantId", tenantId.ToString()} } }); - + Assert.Equal(tenantId, MySimpleDistributedSingleInstanceEventHandler.TenantId); } - - [Fact] - public async Task Event_Should_Published_On_UnitOfWorkComplete() - { - var id = 0; - DistributedEventBus.Subscribe(data => - { - id = data.Value; - return Task.CompletedTask; - }); - - var unitOfWorkManager = GetRequiredService(); - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: true, useOutbox: false); - } - id.ShouldBe(0); - - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: true, useOutbox: false); - await uow.CompleteAsync(); - } - id.ShouldBe(3); - - id = 0; - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: false); - } - id.ShouldBe(3); - } - - [Fact] - public async Task Event_Should_Published_On_UnitOfWorkComplete_UseOutbox() - { - var id = 0; - DistributedEventBus.Subscribe(data => - { - id = data.Value; - return Task.CompletedTask; - }); - - var unitOfWorkManager = GetRequiredService(); - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: true); - } - id.ShouldBe(0); - - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: true); - await uow.CompleteAsync(); - } - id.ShouldBe(3); - - id = 0; - using (var uow = unitOfWorkManager.Begin()) - { - await DistributedEventBus.PublishAsync(new MySimpleEventData(3), onUnitOfWorkComplete: false, useOutbox: false); - } - id.ShouldBe(3); - } } diff --git a/modules/docs/app/VoloDocs.Web/package-lock.json b/modules/docs/app/VoloDocs.Web/package-lock.json index e764349469..9ae1d55d3d 100644 --- a/modules/docs/app/VoloDocs.Web/package-lock.json +++ b/modules/docs/app/VoloDocs.Web/package-lock.json @@ -5,251 +5,269 @@ "requires": true, "dependencies": { "@abp/anchor-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/anchor-js/-/anchor-js-1.1.1.tgz", - "integrity": "sha512-hHyYYJ09hhT5xeQJUsBN43yT+y49FKcigq4Wrx8448TrW7r2NJD5i3Xy3BGEstNSlfcQPTsmciQnbkcU2rX1HQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/anchor-js/-/anchor-js-5.2.1.tgz", + "integrity": "sha512-61+rrfSQyZacqUJ5qQxkoWYffWcd7AArkj8DmEHmFY4e28hH3P9eXMcuGBoJ85pXleAPEmVYswc/xZiTMNHkvg==", "requires": { - "@abp/core": "^1.1.1", - "anchor-js": "^4.2.2" + "@abp/core": "~5.2.1", + "anchor-js": "^4.3.1" } }, "@abp/aspnetcore.mvc.ui": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-1.1.1.tgz", - "integrity": "sha512-wZbptVCSxZzEjkJx+/sWrH9Pikp9nOy7V8Htz+L+S7/qAzfXu5PRVV8ahddfAcDHRk30buRhdbJlCVdt6hkZ6g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-5.2.1.tgz", + "integrity": "sha512-VUSPOKjBSF+NxfwdsEVQte8u7mGP1t7jd1+ej2ND8JEKYJ1Vh7z2mfsT+lQaEJg0JWggU1AxkIMOOfHDNTU3Kg==", "requires": { "ansi-colors": "^4.1.1", "extend-object": "^1.0.0", + "glob": "^7.1.6", "gulp": "^4.0.2", "merge-stream": "^2.0.0", - "path": "^0.12.7", - "rimraf": "^3.0.0" + "micromatch": "^4.0.2" } }, "@abp/aspnetcore.mvc.ui.theme.basic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-1.1.1.tgz", - "integrity": "sha512-ooXtCM3TWN69RU7xs6avnzOQBXzsiHY5BEIogzSBialZC4uG5H56qrIr4MbsFNae+PQM23Mw2tnJ/Z7dutURCQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-5.2.1.tgz", + "integrity": "sha512-DYr9ROcTPfCRHxD1QSWqLZ9+ARbO5p9I6SRo893NtJ39aHacAa9RIAwZmP0JLG0C4hLXfJLKXJ2DpNcwY+ubXA==", "requires": { - "@abp/aspnetcore.mvc.ui.theme.shared": "^1.1.1" + "@abp/aspnetcore.mvc.ui.theme.shared": "~5.2.1" } }, "@abp/aspnetcore.mvc.ui.theme.shared": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-1.1.1.tgz", - "integrity": "sha512-LilSyefzT1+rcTU7vbWxcO8TwBgGZIx6QbMUrDicSTH6LLJ9S5+yNaGNJbbZKDG6qx0BEoC1u8dE8KCUshwxoQ==", - "requires": { - "@abp/aspnetcore.mvc.ui": "^1.1.1", - "@abp/bootstrap": "^1.1.1", - "@abp/bootstrap-datepicker": "^1.1.1", - "@abp/datatables.net-bs4": "^1.1.1", - "@abp/font-awesome": "^1.1.1", - "@abp/jquery-form": "^1.1.1", - "@abp/jquery-validation-unobtrusive": "^1.1.1", - "@abp/lodash": "^1.1.1", - "@abp/luxon": "^1.1.1", - "@abp/malihu-custom-scrollbar-plugin": "^1.1.1", - "@abp/select2": "^1.1.1", - "@abp/sweetalert": "^1.1.1", - "@abp/timeago": "^1.1.1", - "@abp/toastr": "^1.1.1" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-5.2.1.tgz", + "integrity": "sha512-/1C5RyPIRPZT5ir8Len2EnSt1KfWcRdPyn/avAG+9JKBZ8FoUL8mO2/ffESOvikh/wItZZgxJ5VEJVGwHNjgdQ==", + "requires": { + "@abp/aspnetcore.mvc.ui": "~5.2.1", + "@abp/bootstrap": "~5.2.1", + "@abp/bootstrap-datepicker": "~5.2.1", + "@abp/datatables.net-bs5": "~5.2.1", + "@abp/font-awesome": "~5.2.1", + "@abp/jquery-form": "~5.2.1", + "@abp/jquery-validation-unobtrusive": "~5.2.1", + "@abp/lodash": "~5.2.1", + "@abp/luxon": "~5.2.1", + "@abp/malihu-custom-scrollbar-plugin": "~5.2.1", + "@abp/select2": "~5.2.1", + "@abp/sweetalert2": "~5.2.1", + "@abp/timeago": "~5.2.1", + "@abp/toastr": "~5.2.1" } }, "@abp/bootstrap": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/bootstrap/-/bootstrap-1.1.1.tgz", - "integrity": "sha512-OIaGJaizhI8UNfy4bnw2xT2Z0QG7BJJrjxOPGepfd4jn/AUi/vFdOpJFWvu2P9PwSzRmn/LuSlr2WONDOdPVWQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/bootstrap/-/bootstrap-5.2.1.tgz", + "integrity": "sha512-vFW8OxfRhiDkIrDVIn3TyGkGyiCLLFmPMjSOmMg3o2XPdRk5uhwSBzWYpk/m+kmPpP6cEsJMxaHpCsirSlPE+A==", "requires": { - "@abp/core": "^1.1.1", - "bootstrap": "^4.3.1" + "@abp/core": "~5.2.1", + "bootstrap": "^5.1.3" } }, "@abp/bootstrap-datepicker": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/bootstrap-datepicker/-/bootstrap-datepicker-1.1.1.tgz", - "integrity": "sha512-RIQLSrKBu/cTAU2lFenSAoKcMp7wgF4e3nP4/iOu5ZtCgti5vUjMEcqEvBxtlwKTHMXsTG4GtNKqjTwjXMjONQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/bootstrap-datepicker/-/bootstrap-datepicker-5.2.1.tgz", + "integrity": "sha512-UPdVu9t7XybINSfonQN0DB9Lpz1r5vCz7F8CMpbjQprvPmsFmkAZyY0p6MS3kGO5eu5rlpGAGPBGOTeSfEp9ww==", "requires": { "bootstrap-datepicker": "^1.9.0" } }, "@abp/clipboard": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/clipboard/-/clipboard-1.1.1.tgz", - "integrity": "sha512-O6b7VCAh2mxjkPhgUZYQBhXf0u+dWRECBOYN4KxYiKfk+2xL8X+34Ls7hVLbyuD+4xCcNirMZIG5CnS8auHC/A==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/clipboard/-/clipboard-5.2.1.tgz", + "integrity": "sha512-aouNTDz8t+8M4O2a+UsEdtABRsyhvzGpXqCG2+LYE1vA3I+CKhglkvEFp+GyIgWsipEHY1U1w6V3qZtcRINn+A==", "requires": { - "@abp/core": "^1.1.1", - "clipboard": "^2.0.4" + "@abp/core": "~5.2.1", + "clipboard": "^2.0.8" } }, "@abp/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/core/-/core-1.1.1.tgz", - "integrity": "sha512-OvUG7xRvk8nSqwC1s45YPnTuhC2OWe1AVa1nnC6FVHMH/g1Je7UJwnbu47K7uNS+lDRJUIktNbufYKiwutEjRg==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/core/-/core-5.2.1.tgz", + "integrity": "sha512-FDOhIPjig3oGxkbadJZzFSC1ZHzgQV4R75fsDNH56lQ9mTyRUPQdg0Y54eCtY7yOSjiJOctOUUWHaxoFG7frGQ==", + "requires": { + "@abp/utils": "~5.2.1" + } }, "@abp/datatables.net": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/datatables.net/-/datatables.net-1.1.1.tgz", - "integrity": "sha512-y76IDBlgc0n1YgQqJ+9cfzXpLwr2arhdIfSmqX+qRXz6GfVNY7e3JijkFSgDKUzKGYo1HZMzgJmDmeNIRwMZsQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/datatables.net/-/datatables.net-5.2.1.tgz", + "integrity": "sha512-6Q3+W+d8e4TMAkZr/IdPDQuL1v+tjbS50ChLvrJX/BLb4fBhu1LGJWWKzKJFj721DwIsuQQiM4uq9xX/TjiS0w==", "requires": { - "@abp/core": "^1.1.1", - "datatables.net": "^1.10.20" + "@abp/jquery": "~5.2.1", + "datatables.net": "^1.11.4" } }, - "@abp/datatables.net-bs4": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/datatables.net-bs4/-/datatables.net-bs4-1.1.1.tgz", - "integrity": "sha512-t40xQIGBMLPZiSbcZHW3AwE8uk+xcl7OitBT1jym0XPKVtgJsHez3ynDE5v/PjHe+ColCG8lTRjRnNoXo5dzDw==", + "@abp/datatables.net-bs5": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/datatables.net-bs5/-/datatables.net-bs5-5.2.1.tgz", + "integrity": "sha512-B8lSAeMM9qOwYbDK/Dhp7BX5lFaCpao4RCPcSqgFrye8vlH8bcobmp4tMD23r24y/gRIEuQBcKzp0Lf0OUpLhA==", "requires": { - "@abp/datatables.net": "^1.1.1", - "datatables.net-bs4": "^1.10.20" + "@abp/datatables.net": "~5.2.1", + "datatables.net-bs5": "^1.11.4" } }, "@abp/docs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/docs/-/docs-1.1.1.tgz", - "integrity": "sha512-z2Dk7EOhBdFo6BYbgccD91PIg37L2ehkgRT4RWf61dRyjPAQgPwwwyVSqApFmKveWPCzreHoTPKbnrU7EzAssw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/docs/-/docs-5.2.1.tgz", + "integrity": "sha512-WZCCY73vyIpRu7hypPiP9CRr4Bvzkv3up0WeGQ4rK9LiZWNSxG9PXv4lYeD4cuHg0zgxH9d/6toYToaIJNqDCQ==", "requires": { - "@abp/anchor-js": "^1.1.1", - "@abp/clipboard": "^1.1.1", - "@abp/malihu-custom-scrollbar-plugin": "^1.1.1", - "@abp/popper.js": "^1.1.1", - "@abp/prismjs": "^1.1.1" + "@abp/anchor-js": "~5.2.1", + "@abp/clipboard": "~5.2.1", + "@abp/malihu-custom-scrollbar-plugin": "~5.2.1", + "@abp/popper.js": "~5.2.1", + "@abp/prismjs": "~5.2.1" } }, "@abp/font-awesome": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/font-awesome/-/font-awesome-1.1.1.tgz", - "integrity": "sha512-6RHbixi7IVWAb3JCHrUmEYD3HmAH4R75Nuo54LvFzATrh4G6gdBONIeDuTo79OTxe4Zhp+WLxeA49Y21kt77mg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/font-awesome/-/font-awesome-5.2.1.tgz", + "integrity": "sha512-9fAUdA9QeNRMjp6v8i6EOR480bjB4OzqzriFCKUu4k6VwbA6PxUsJIRFyKIt5UpC12Zqdhpkyj0iG6tE0nRekQ==", "requires": { - "@abp/core": "^1.1.1", - "@fortawesome/fontawesome-free": "^5.11.2" + "@abp/core": "~5.2.1", + "@fortawesome/fontawesome-free": "^5.15.4" } }, "@abp/jquery": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/jquery/-/jquery-1.1.1.tgz", - "integrity": "sha512-kj4BTtXF0VbCzCqRXnRVEbGndR3F8NlbBhVQN6BQktOuZta3fvx7f2+pSok8vQv0ddmqUFY7FTT2Ei3l4363LQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/jquery/-/jquery-5.2.1.tgz", + "integrity": "sha512-FiIRnDx/gm6JR8QljiulwCc5d8+YC123X0qxMIBI8IY9vznEX+Jk48jYG8fLABnRqKEIYfV8UsYSK8IJx3mcSg==", "requires": { - "@abp/core": "^1.1.1", - "jquery": "^3.4.1" + "@abp/core": "~5.2.1", + "jquery": "~3.6.0" } }, "@abp/jquery-form": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/jquery-form/-/jquery-form-1.1.1.tgz", - "integrity": "sha512-AIIdN36f8xwr4LgiNnBHohJ5tlxh/r+DuDtXcScpZN6GWBE+XgUotN0pZIIva82IxCyUNdDudzgluX9IjI+00w==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/jquery-form/-/jquery-form-5.2.1.tgz", + "integrity": "sha512-L7uKs7vReOQEETG9xIDq5aXjshbaPa+ZZQcCbn2uwY813e0ErS7Rb1mnowEt/LNEB02AtLet1B4TDVwZUl1uXQ==", "requires": { - "@abp/jquery": "^1.1.1", - "jquery-form": "^4.2.2" + "@abp/jquery": "~5.2.1", + "jquery-form": "^4.3.0" } }, "@abp/jquery-validation": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/jquery-validation/-/jquery-validation-1.1.1.tgz", - "integrity": "sha512-YvAjIW8epp+ddu01BTUkZWPfEADAvNPJeUrrZ6OpcPWM15Tf+ddr4ATgJ1LCg0Bh5F09iQC855osow3lt8sc7g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/jquery-validation/-/jquery-validation-5.2.1.tgz", + "integrity": "sha512-Rr/+SWGlXJ53jfysMB/HVNZqsJKCF3rg23ip2Kg6Q+kQTvWVRE3tpkpoBJczOii5tPUk/A/lsJKgRlcsnP0ASw==", "requires": { - "@abp/jquery": "^1.1.1", - "jquery-validation": "^1.19.1" + "@abp/jquery": "~5.2.1", + "jquery-validation": "^1.19.3" } }, "@abp/jquery-validation-unobtrusive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-1.1.1.tgz", - "integrity": "sha512-q1b0KG8l3DXUiW8JXdq9l1jR/CwgzrZdxwdKGLB2J/oxHlywQIb7yrjR6WGCshjPpcx2SkOL0j/ZXMIMh533hQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-5.2.1.tgz", + "integrity": "sha512-uZ36D1FfoLdBb6h44fQ3kZuTk4gJ5yzhyOprkgMsGAJDVakX7w/W4V3ThpiEO+iUpNKTboVIhW2QQ0AXK9rrsg==", "requires": { - "@abp/jquery-validation": "^1.1.1", - "jquery-validation-unobtrusive": "^3.2.11" + "@abp/jquery-validation": "~5.2.1", + "jquery-validation-unobtrusive": "^3.2.12" } }, "@abp/lodash": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/lodash/-/lodash-1.1.1.tgz", - "integrity": "sha512-nH7bRS28Tf4hEXcpKHd1IM+MzYTqX8t3htGmsLX4UESQd52eODYOIldtX6gm3OW1O6ECwW6si/o0M2pTEpQqvg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/lodash/-/lodash-5.2.1.tgz", + "integrity": "sha512-ILg3X5tTH2HhJMRmg7BP/r+Kstm/nf+0aNQ2exsJoMMnKE7CC0eYQjpSgrze6GwG3a13eamyTlrz+RrlIm5IBA==", "requires": { - "@abp/core": "^1.1.1", - "lodash": "^4.17.15" + "@abp/core": "~5.2.1", + "lodash": "^4.17.21" } }, "@abp/luxon": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/luxon/-/luxon-1.1.1.tgz", - "integrity": "sha512-WNu8JRSb5FDXfcDwjMYyeYeUN48uuDc/I2cdo3xd1rcY+lbmbzxoG9IYOlE8cRHdgX3z82qsZXFs2lcAy0Le2g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/luxon/-/luxon-5.2.1.tgz", + "integrity": "sha512-D3KVsba969UBYktdbCxq1JQp4kYZ1S7rIMymDJMBoHByXxwwdeXMkvuphAifBmSYTt3K6bNoZdR0VxtnNlPn2A==", "requires": { - "luxon": "^1.21.3" + "@abp/core": "~5.2.1", + "luxon": "^2.3.0" } }, "@abp/malihu-custom-scrollbar-plugin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-1.1.1.tgz", - "integrity": "sha512-n4b4QK/L1Czdx0oOpUR/bWjK9VENexfUSV/aMjwzHhDmEFABAmEfhIpudCYDwewGswrd7C9agmBvakv2rwPQeA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-5.2.1.tgz", + "integrity": "sha512-5mvABMCT7tiwPl1vUK8kriN/SRi2gC4VqkEuxghT7uBQG9Cqh5jhJrl80M9ZK/oQFind3r6+SF8OlfwF8yvxHQ==", "requires": { - "@abp/core": "^1.1.1", + "@abp/core": "~5.2.1", "malihu-custom-scrollbar-plugin": "^3.1.5" } }, "@abp/popper.js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/popper.js/-/popper.js-1.1.1.tgz", - "integrity": "sha512-heR73cqmMsVPNgsPxBYbkvc842R3hEEuDAj4oaXZwVTeWXayU6TdDcGdIrfwMZwW2eWivYNnO0bMOVmuhZKTTQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/popper.js/-/popper.js-5.2.1.tgz", + "integrity": "sha512-poQhd5EYjU2/udJWlDEd5mIPWmw6AzNOzAd5V4OUHMai+BHeuhIXQ6mopvxf9lpzytGoFe2ZIiQ547Wfq4Fl/Q==", "requires": { - "@abp/core": "^1.1.1", - "popper.js": "^1.16.0" + "@abp/core": "~5.2.1", + "@popperjs/core": "^2.11.2" } }, "@abp/prismjs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/prismjs/-/prismjs-1.1.1.tgz", - "integrity": "sha512-kZh2imqVTMDWmE2v+S4wMsigu/hSyVaz3VvZzGctFNctzC17LeD6z6ymfrtQ5BPJbCxGSpHwOt4/Y8bycPTEuQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/prismjs/-/prismjs-5.2.1.tgz", + "integrity": "sha512-YNgcM7Kvmu3hGXJh4B8gl7rLzC28VuZYYP7AVptVSbTz/n6usCo21evG/st8L3vXixuQkvnNpBFgacJnHdSJZQ==", "requires": { - "@abp/core": "^1.1.1", - "prismjs": "^1.17.1" + "@abp/clipboard": "~5.2.1", + "@abp/core": "~5.2.1", + "prismjs": "^1.26.0" } }, "@abp/select2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/select2/-/select2-1.1.1.tgz", - "integrity": "sha512-t0qcJhD+uo2+XWr4nmMQLAx7MRGQUBdZ81YmGty045ReoSaEKQf4haLkzBcMzpBRusiyMQO/PbxjtwMw/xJQTQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/select2/-/select2-5.2.1.tgz", + "integrity": "sha512-JH/PqOxhTY05sUyN7of6TNai0W4M3N3OF3Hlwmr8i7hNdYfFwJvQnQzKeKrk/vt8Hv44/JTQDlNKU02BmSBfOQ==", "requires": { - "@abp/core": "^1.1.1", - "select2": "^4.0.12" + "@abp/core": "~5.2.1", + "select2": "^4.0.13" } }, - "@abp/sweetalert": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/sweetalert/-/sweetalert-1.1.1.tgz", - "integrity": "sha512-V6K/qg7J/bdFmom2kaXYeiLvcmLHFl+MacPX4yYAK2biZdb2pWOkUdmcAzZdOT+UruKfLRhvraVC2uXDySi9NA==", + "@abp/sweetalert2": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/sweetalert2/-/sweetalert2-5.2.1.tgz", + "integrity": "sha512-laaF/5WhYw+hNJRTfMzO93fVhaYqnnOcQTUlkGgsZMe2gwebyX73VI8O8Xw7zXmN1Tu/JwqRI46qiafDrPFTLg==", "requires": { - "@abp/core": "^1.1.1", - "sweetalert": "^2.1.2" + "@abp/core": "~5.2.1", + "sweetalert2": "^11.3.6" } }, "@abp/timeago": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/timeago/-/timeago-1.1.1.tgz", - "integrity": "sha512-QYYih/4n6XhCqkRw7fBfyg58T5CHqJHyz7SAfq86RiKAJ4jVtjdSVxj3XKxz8eCb56wZGsO1xXXStI3vdLwwNw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/timeago/-/timeago-5.2.1.tgz", + "integrity": "sha512-xmgqKEKusB6pcqFhMaz8RTi886ad8RrRMYgMWSw4Zjk1Lr9EqQwKtcE43Ve5XWJamh2Wpk8H7IKLQKHfrV12oA==", "requires": { - "@abp/jquery": "^1.1.1", + "@abp/jquery": "~5.2.1", "timeago": "^1.6.7" } }, "@abp/toastr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@abp/toastr/-/toastr-1.1.1.tgz", - "integrity": "sha512-GDewBppm+0FO6kTTy0huczoH9P5q6lFicHFAoEawAMkuWJFW/Ihv/YnEvKGDQwGftuVSWexfqBMN/RZ5YSOiGQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/toastr/-/toastr-5.2.1.tgz", + "integrity": "sha512-HrnIzvM9LgQdzlmLmvHUVSG4PmWfx9YuozxkFTv+AGa2FAPby5W9hbQ025ry3bPkU9lGWSu/w7JSDqoiL16bPA==", "requires": { - "@abp/jquery": "^1.1.1", + "@abp/jquery": "~5.2.1", "toastr": "^2.1.4" } }, + "@abp/utils": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@abp/utils/-/utils-5.2.1.tgz", + "integrity": "sha512-9hxI24aRZCnxCP+WsOoCltSg4YqG9WtW06t9/f6hFO9B0udXIKyV+95Ndipca/R1G94Snx81ifSwAa+DHbFfvQ==", + "requires": { + "just-compare": "^1.3.0" + } + }, "@fortawesome/fontawesome-free": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz", - "integrity": "sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==" + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" + }, + "@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" }, "anchor-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/anchor-js/-/anchor-js-4.2.2.tgz", - "integrity": "sha512-Rg1tGaG4K3avYqDh7rOYCE/odWxpUiHStnlKL/bGOt9cl6NjR06zhPGVQcCAjE5PT48oQeHVgqNmLzxh0Kuk4A==" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/anchor-js/-/anchor-js-4.3.1.tgz", + "integrity": "sha512-TziERoibspey7KSm95oIdzTxiogXonJl7inQI07Y3cI25DKQaLkUftB7RhCuSb1GcwunHL6/PcIKM4dDUb9xYQ==" }, "ansi-colors": { "version": "4.1.1", @@ -281,6 +299,36 @@ "requires": { "micromatch": "^3.1.4", "normalize-path": "^2.1.1" + }, + "dependencies": { + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "append-buffer": { @@ -441,9 +489,9 @@ } }, "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base": { "version": "0.11.2", @@ -510,9 +558,9 @@ } }, "bootstrap": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz", - "integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==" + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==" }, "bootstrap-datepicker": { "version": "1.9.0", @@ -564,9 +612,9 @@ "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "cache-base": { "version": "1.0.1", @@ -584,6 +632,15 @@ "unset-value": "^1.0.0" } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", @@ -606,13 +663,6 @@ "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", "upath": "^1.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - } } }, "class-utils": { @@ -637,9 +687,9 @@ } }, "clipboard": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz", - "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.10.tgz", + "integrity": "sha512-cz3m2YVwFz95qSEbCDi2fzLN/epEN9zXBvfgAoGkvGOJZATMl9gtTDVOtBYkx2ODUJl2kvmud7n32sV2BpYR4g==", "requires": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -732,9 +782,9 @@ } }, "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "requires": { "safe-buffer": "~5.1.1" } @@ -745,18 +795,25 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "copy-props": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", - "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } } }, "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "d": { "version": "1.0.1", @@ -768,19 +825,19 @@ } }, "datatables.net": { - "version": "1.10.20", - "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.20.tgz", - "integrity": "sha512-4E4S7tTU607N3h0fZPkGmAtr9mwy462u+VJ6gxYZ8MxcRIjZqHy3Dv1GNry7i3zQCktTdWbULVKBbkAJkuHEnQ==", + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.5.tgz", + "integrity": "sha512-nlFst2xfwSWaQgaOg5sXVG3cxYC0tH8E8d65289w9ROgF2TmLULOOpcdMpyxxUim/qEwVSEem42RjkTWEpr3eA==", "requires": { "jquery": ">=1.7" } }, - "datatables.net-bs4": { - "version": "1.10.20", - "resolved": "https://registry.npmjs.org/datatables.net-bs4/-/datatables.net-bs4-1.10.20.tgz", - "integrity": "sha512-kQmMUMsHMOlAW96ztdoFqjSbLnlGZQ63iIM82kHbmldsfYdzuyhbb4hTx6YNBi481WCO3iPSvI6YodNec46ZAw==", + "datatables.net-bs5": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-1.11.5.tgz", + "integrity": "sha512-1zyh972GtuK1uAb9h8nP3jJ7f/3UgCDq69LAaZS2bVd4mEHECJ6vrZLacxrkOHOs/q/H3v5sEMeZ46vXz8ox4w==", "requires": { - "datatables.net": "1.10.20", + "datatables.net": ">=1.11.3", "jquery": ">=1.7" } }, @@ -823,11 +880,12 @@ "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=" }, "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "requires": { - "object-keys": "^1.0.12" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, "define-property": { @@ -914,13 +972,13 @@ } }, "es5-ext": { - "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", - "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "version": "0.10.60", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.60.tgz", + "integrity": "sha512-jpKNXIt60htYG59/9FGf2PYT3pwMpnEbNKysU+k/4FGwyGtMotOvcZOuW+EmXXYASRqYSXQfGL5cVIthOTgbkg==", "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.3", - "next-tick": "~1.0.0" + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" } }, "es6-iterator": { @@ -933,11 +991,6 @@ "es6-symbol": "^3.1.1" } }, - "es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=" - }, "es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -999,17 +1052,17 @@ } }, "ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "requires": { - "type": "^2.0.0" + "type": "^2.5.0" }, "dependencies": { "type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", - "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", + "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==" } } }, @@ -1112,6 +1165,11 @@ "time-stamp": "^1.0.0" } }, + "fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,6 +1215,28 @@ "is-glob": "^4.0.0", "micromatch": "^3.0.4", "resolve-dir": "^1.0.1" + }, + "dependencies": { + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } } }, "fined": { @@ -1221,487 +1301,13 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "optional": true - } + "nan": "^2.12.1" } }, "function-bind": { @@ -1714,15 +1320,25 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1769,15 +1385,16 @@ } }, "glob-watcher": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", - "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", "requires": { "anymatch": "^2.0.0", "async-done": "^1.2.0", "chokidar": "^2.0.0", "is-negated-glob": "^1.0.0", "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", "object.defaults": "^1.1.0" } }, @@ -1820,9 +1437,9 @@ } }, "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==" + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "gulp": { "version": "4.0.2", @@ -1844,9 +1461,9 @@ } }, "gulp-cli": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "requires": { "ansi-colors": "^1.0.1", "archy": "^1.0.0", @@ -1856,7 +1473,7 @@ "copy-props": "^2.0.1", "fancy-log": "^1.3.2", "gulplog": "^1.0.0", - "interpret": "^1.1.0", + "interpret": "^1.4.0", "isobject": "^3.0.1", "liftoff": "^3.1.0", "matchdep": "^2.0.0", @@ -1864,7 +1481,7 @@ "pretty-hrtime": "^1.0.0", "replace-homedir": "^1.0.0", "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", + "v8flags": "^3.2.0", "yargs": "^7.1.0" } } @@ -1878,10 +1495,26 @@ "glogg": "^1.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, "has-value": { "version": "1.0.0", @@ -1921,9 +1554,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "inflight": { "version": "1.0.6", @@ -1940,14 +1573,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" }, "invert-kv": { "version": "1.0.0", @@ -1999,6 +1632,14 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -2053,9 +1694,9 @@ } }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "requires": { "is-extglob": "^2.1.1" } @@ -2138,14 +1779,14 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" }, "jquery-form": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/jquery-form/-/jquery-form-4.2.2.tgz", - "integrity": "sha512-HJTef7DRBSg8ge/RNUw8rUTTtB3l8ozO0OhD16AzDl+eIXp4skgCqRTd9fYPsOzL+pN6+1B9wvbTLGjgikz8Tg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jquery-form/-/jquery-form-4.3.0.tgz", + "integrity": "sha512-q3uaVCEWdLOYUCI6dpNdwf/7cJFOsUgdpq6r0taxtGQ5NJSkOzofyWm4jpOuJ5YxdmL1FI5QR+q+HB63HHLGnQ==", "requires": { "jquery": ">=1.7.2" } @@ -2156,16 +1797,16 @@ "integrity": "sha1-BvAzXxbjU6aV5yBr9QUDy1I6buU=" }, "jquery-validation": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/jquery-validation/-/jquery-validation-1.19.1.tgz", - "integrity": "sha512-QNnrZBqSltWUEJx+shOY5WtfrIb0gWmDjFfQP8rZKqMMSfpRSwEkSqhfHPvDfkObD8Hnv5KHSYI8yg73sVFdqA==" + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/jquery-validation/-/jquery-validation-1.19.3.tgz", + "integrity": "sha512-iXxCS5W7STthSTMFX/NDZfWHBLbJ1behVK3eAgHXAV8/0vRa9M4tiqHvJMr39VGWHMGdlkhrtrkBuaL2UlE8yw==" }, "jquery-validation-unobtrusive": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.2.11.tgz", - "integrity": "sha512-3FQPllaWdD+Aq55zJLGSW39+eXPDz1HhwAvrSwYi8zHQ8DVcu5IJ1HVeTiCl0BnCnrIBvfFU3zEB/DrGdcoRIQ==", + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.2.12.tgz", + "integrity": "sha512-kPixGhVcuat7vZXngGFfSIksy4VlzZcHyRgnBIZdsfVneCU+D5sITC8T8dD/9c9K/Q+qkMlgp7ufJHz93nKSuQ==", "requires": { - "jquery": ">=1.8", + "jquery": "^3.5.1", "jquery-validation": ">=1.16" } }, @@ -2174,10 +1815,15 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" }, + "just-compare": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-1.5.1.tgz", + "integrity": "sha512-xDEEFHNIyJNmN4uo/2RVeUcay9THtN/5ka/iw98Y/gsa8w9KXZQuyaf5eFUY6VlntA2+G+bdPmdhqqTs7T+BRw==" + }, "just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", + "integrity": "sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==" }, "kind-of": { "version": "6.0.3", @@ -2194,9 +1840,9 @@ } }, "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "requires": { "readable-stream": "^2.0.5" } @@ -2245,14 +1891,14 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "luxon": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.22.2.tgz", - "integrity": "sha512-vq6eSaOOw1fKob+JXwfu0e3/UFUT4G4HTFRJab7dch8J1OdOGW/vXqCiJsY7rm2In+5gKNYx0EtnYT0Tc5V4Qw==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.3.2.tgz", + "integrity": "sha512-MlAQQVMFhGk4WUA6gpfsy0QycnKP0+NlCBJRVRNPxxSIbjrCbQ65nrpJD3FVyJNZLuJ0uoqL57ye6BmDYgHaSw==" }, "make-iterator": { "version": "1.0.1", @@ -2312,6 +1958,26 @@ "requires": { "is-extglob": "^2.1.0" } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } } } }, @@ -2321,29 +1987,49 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } @@ -2378,9 +2064,9 @@ "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==" }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, "nanomatch": { @@ -2402,9 +2088,9 @@ } }, "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, "normalize-package-data": { "version": "2.5.0", @@ -2418,12 +2104,9 @@ } }, "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "now-and-later": { "version": "2.0.1", @@ -2480,14 +2163,14 @@ } }, "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, "object.defaults": { @@ -2584,15 +2267,6 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, - "path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", - "requires": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", @@ -2612,9 +2286,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-root": { "version": "0.1.1", @@ -2639,6 +2313,11 @@ "pinkie-promise": "^2.0.0" } }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2657,11 +2336,6 @@ "pinkie": "^2.0.0" } }, - "popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -2673,28 +2347,15 @@ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=" }, "prismjs": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.19.0.tgz", - "integrity": "sha512-IVFtbW9mCWm9eOIaEkNyo2Vl4NnEifis2GQ7/MLRG5TQe6t+4Sj9J5QWI9i3v+SS43uZBlCAOn+zYTVYQcPXJw==", - "requires": { - "clipboard": "^2.0.0" - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.28.0.tgz", + "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==" }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "promise-polyfill": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", - "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" - }, "pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", @@ -2755,6 +2416,28 @@ "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", "readable-stream": "^2.0.2" + }, + "dependencies": { + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } } }, "rechoir": { @@ -2799,9 +2482,9 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==" }, "repeat-string": { "version": "1.6.1", @@ -2809,9 +2492,9 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==" }, "replace-homedir": { "version": "1.0.0", @@ -2834,11 +2517,13 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" }, "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "requires": { - "path-parse": "^1.0.6" + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "resolve-dir": { @@ -2868,14 +2553,6 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3053,9 +2730,9 @@ } }, "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" }, "sparkles": { "version": "1.0.1", @@ -3063,32 +2740,32 @@ "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==" }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==" }, "split-string": { "version": "3.1.0", @@ -3166,6 +2843,11 @@ "is-utf8": "^0.2.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "sver-compat": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", @@ -3175,14 +2857,10 @@ "es6-symbol": "^3.1.1" } }, - "sweetalert": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/sweetalert/-/sweetalert-2.1.2.tgz", - "integrity": "sha512-iWx7X4anRBNDa/a+AdTmvAzQtkN1+s4j/JJRWlHpYE8Qimkohs8/XnFcWeYHH2lMA8LRCa5tj2d244If3S/hzA==", - "requires": { - "es6-object-assign": "^1.1.0", - "promise-polyfill": "^6.0.2" - } + "sweetalert2": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.8.tgz", + "integrity": "sha512-BDS/+E8RwaekGSxCPUbPnsRAyQ439gtXkTF/s98vY2l9DaVEOMjGj1FaQSorfGREKsbbxGSP7UXboibL5vgTMA==" }, "through2": { "version": "2.0.5", @@ -3299,15 +2977,16 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" }, "undertaker": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", - "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", "requires": { "arr-flatten": "^1.0.1", "arr-map": "^2.0.0", "bach": "^1.0.0", "collection-map": "^1.0.0", "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", "last-run": "^1.1.0", "object.defaults": "^1.0.0", "object.reduce": "^1.0.0", @@ -3390,30 +3069,15 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "requires": { "homedir-polyfill": "^1.0.1" } @@ -3433,9 +3097,9 @@ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=" }, "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "requires": { "clone": "^2.1.1", "clone-buffer": "^1.0.0", @@ -3481,6 +3145,16 @@ "now-and-later": "^2.0.0", "remove-bom-buffer": "^3.0.0", "vinyl": "^2.0.0" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "which": { @@ -3516,14 +3190,14 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" }, "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "requires": { "camelcase": "^3.0.0", "cliui": "^3.2.0", @@ -3537,15 +3211,16 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" + "yargs-parser": "^5.0.1" } }, "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "requires": { - "camelcase": "^3.0.0" + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" } } } diff --git a/modules/docs/app/VoloDocs.Web/yarn.lock b/modules/docs/app/VoloDocs.Web/yarn.lock index 802094ec7c..8e5f9ddd6d 100644 --- a/modules/docs/app/VoloDocs.Web/yarn.lock +++ b/modules/docs/app/VoloDocs.Web/yarn.lock @@ -2009,9 +2009,9 @@ pretty-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" prismjs@^1.26.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" - integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== + version "1.28.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6" + integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw== process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.0" diff --git a/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs b/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs index 77f5bec551..42c1717690 100644 --- a/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs +++ b/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Components.LayoutHook; using Volo.Abp.AspNetCore.Mvc.UI.Packages; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; @@ -16,6 +17,7 @@ using Volo.Docs.Bundling; using Volo.Docs.HtmlConverting; using Volo.Docs.Localization; using Volo.Docs.Markdown; +using Volo.Docs.Pages.Shared.Components.Head; namespace Volo.Docs { @@ -91,6 +93,11 @@ namespace Volo.Docs { options.DisableModule(DocsRemoteServiceConsts.ModuleName); }); + + Configure(options => + { + options.Add(LayoutHooks.Head.Last, typeof(HeadViewComponent)); + }); } } } 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 c0ea0c4d7c..5ca2b7fbd9 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 @@ -11,38 +11,42 @@ @using Volo.Docs.Localization @using Volo.Docs.Pages.Documents.Project @using Volo.Docs.Pages.Documents.Shared.ErrorComponent +@using Volo.Docs.Pages.Shared.Components.Head @inject IThemeManager ThemeManager @inject IPageLayout PageLayout @inject IHtmlLocalizer L + @model IndexModel @{ ViewBag.FluidLayout = true; Layout = ThemeManager.CurrentTheme.GetEmptyLayout(); PageLayout.Content.Title = Model.DocumentName?.Replace("-", " "); ViewBag.Description = Model.GetDescription(); + ViewBag.CanonicalUrl = Model.IsLatestVersion ? null : Model.GetFullUrlOfTheLatestDocument(); //issue #12355 } + @section styles { - - - - - - - + + + + + + + } @section scripts { - - - - - - - - - - - - + + + + + + + + + + + + } @if (Model.LoadSuccess) { @@ -93,8 +97,8 @@ @@ -112,11 +116,11 @@ - + @@ -131,11 +135,11 @@
@* - - *@ + + *@
@@ -149,11 +153,11 @@
+ id="filter" + type="search" + data-search-url="@Model." + placeholder="@L["FilterTopics"].Value" + aria-label="Filter">
@@ -168,13 +172,13 @@ else { + version="@(Model.LatestVersionInfo == null || Model.LatestVersionInfo.IsSelected ? DocsAppConsts.Latest : Model.Version)" + project-name="@Model.ProjectName" + project-format="@Model.Project.Format" + selected-document-name="@Model.DocumentNameWithExtension" + language="@Model.LanguageCode" + id="sidebar-scroll" + class="nav nav-list"> } @@ -194,11 +198,11 @@ + id="fullsearch" + type="search" + data-fullsearch-url="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" + placeholder="@L["FullSearch"].Value" + aria-label="Filter"> } @@ -210,15 +214,15 @@ @(L["Contributors"].Value) - @foreach (var contributor in Model.Document.Contributors.OrderByDescending(c=> c.CommitCount).ToList()) + @foreach (var contributor in Model.Document.Contributors.OrderByDescending(c => c.CommitCount).ToList()) { Avatar + class="rounded-circle" + alt="Avatar" + height="21" + width="21" + title="@contributor.Username" /> } } @@ -261,8 +265,8 @@
@(parameter.DisplayName)