Browse Source

Merge branch 'dev' into community-3.2

pull/5262/head
berkansasmaz 6 years ago
parent
commit
9aa29675f9
  1. 0
      LICENSE.md
  2. 15
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  3. 7
      common.props
  4. 6
      docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md
  5. 405
      docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/POST.md
  6. BIN
      docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/abp-generate-proxy-output.png
  7. BIN
      docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/swagger-book-list.png
  8. 46
      docs/en/CLI.md
  9. 31
      docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md
  10. 18
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/Using-DevExtreme-Components-With-The-ABP-Framework.md
  11. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/both-example-result.png
  12. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-app-contract.png
  13. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-application.png
  14. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-final.png
  15. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-web.png
  16. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/devexp-result.gif
  17. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/devextreme-js.png
  18. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/gulp.png
  19. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/initial-project.png
  20. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-app-contract.png
  21. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-application.png
  22. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-final.png
  23. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-web.png
  24. 0
      docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/wwwroot-lib.png
  25. 81
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md
  26. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-simple-property.jpg
  27. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-user-navigation.jpg
  28. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-appuserdto.jpg
  29. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-mapping.jpg
  30. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-note-entity.jpg
  31. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/final-page.jpg
  32. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal.jpg
  33. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal2.jpg
  34. 2
      docs/en/Customizing-Application-Modules-Overriding-Services.md
  35. 167
      docs/en/Distributed-Event-Bus-Kafka-Integration.md
  36. 1
      docs/en/Distributed-Event-Bus.md
  37. 153
      docs/en/Tutorials/Part-10.md
  38. 13
      docs/en/Tutorials/Part-2.md
  39. 58
      docs/en/Tutorials/Part-3.md
  40. 19
      docs/en/Tutorials/Part-9.md
  41. BIN
      docs/en/Tutorials/images/bookstore-angular-author-selection.png
  42. BIN
      docs/en/Tutorials/images/bookstore-angular-service-proxy-author-2.png
  43. BIN
      docs/en/Tutorials/images/bookstore-books-with-authorname-angular.png
  44. BIN
      docs/en/Tutorials/images/generated-proxies-3.png
  45. 109
      docs/en/UI/Angular/Environment.md
  46. 2
      docs/en/UI/Angular/Migration-Guide-v3.md
  47. 12
      docs/en/docs-nav.json
  48. 2
      docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md
  49. 163
      docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md
  50. 1
      docs/zh-Hans/Distributed-Event-Bus.md
  51. 4
      docs/zh-Hans/docs-nav.json
  52. 14
      framework/Volo.Abp.sln
  53. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs
  54. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs
  55. 1
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs
  56. 16
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/StarRatingSvg/StarRatingSvgScriptContributor.cs
  57. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/StarRatingSvg/StarRatingSvgStyleContributor.cs
  58. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  59. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
  60. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
  61. 832
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs
  62. 144
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs
  63. 11
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs
  64. 87
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  65. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs
  66. 20
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  67. 68
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs
  68. 3
      framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml
  69. 30
      framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xsd
  70. 16
      framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj
  71. 27
      framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs
  72. 12
      framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs
  73. 208
      framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
  74. 3
      framework/src/Volo.Abp.Kafka/FodyWeavers.xml
  75. 30
      framework/src/Volo.Abp.Kafka/FodyWeavers.xsd
  76. 17
      framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj
  77. 31
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs
  78. 22
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs
  79. 109
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs
  80. 10
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs
  81. 11
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs
  82. 19
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs
  83. 11
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs
  84. 10
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs
  85. 35
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs
  86. 156
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs
  87. 32
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs
  88. 102
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs
  89. 27
      framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs
  90. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json
  91. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json
  92. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json
  93. 16
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js
  94. 16
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/jquery/abp.jquery.js
  95. 222
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock
  96. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json
  97. 16
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js
  98. 16
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/jquery/abp.jquery.js
  99. 236
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock
  100. 4
      modules/blogging/app/Volo.BloggingTestApp/package.json

0
LICENSE → LICENSE.md

15
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -64,15 +64,12 @@
"CoverImage": "Cover Image",
"ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!",
"Optional": "Optional",
"UpdateUserNameInfo": "Name",
"UpdateUserSurnameInfo": "Surname",
"UpdateUserWebSiteInfo": "Example: https://abp.io",
"UpdateUserTwitterInfo": "Example: abpframework",
"UpdateUserGithubInfo": "Example: volosoft",
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/company/volosoft",
"UpdateUserCompanyInfo": "Example: Volosoft Computer & Technology",
"UpdateUserJobTitleInfo": "Example: Software Engineer Intern",
"UpdateUserBiographyInfo": "About you",
"UpdateUserWebSiteInfo": "Example: https://johndoe.com",
"UpdateUserTwitterInfo": "Example: johndoe",
"UpdateUserGithubInfo": "Example: johndoe",
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...",
"UpdateUserCompanyInfo": "Example: Volosoft",
"UpdateUserJobTitleInfo": "Example: Software Developer",
"UserName": "UserName",
"Company": "Company",
"PersonalWebsite": "Personal Website",

7
common.props

@ -1,15 +1,16 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>3.1.0</Version>
<Version>3.2.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/abpframework/abp/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/abpframework/abp/</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.3" PrivateAssets="All" />
<None Include="LICENSE.md" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

6
docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md

@ -1,6 +1,6 @@
# ABP Framework v3.1 RC.1 Has Been Released
# ABP Framework v3.1 RC Has Been Released
Today, we are releasing the **ABP Framework version 3.1 Release Candidate 1** (RC.1). The development cycle for this version was **~7 weeks**. It was the longest development cycle for a feature version release ever. We have completed **~150 issues**, merged **~150 PRs** and made **~1,000 commits** only in the main [abp repository](https://github.com/abpframework/abp). See the related [milestone](https://github.com/abpframework/abp/milestone/38?closed=1) on GitHub.
Today, we are releasing the **ABP Framework version 3.1 Release Candidate** (RC). The development cycle for this version was **~7 weeks**. It was the longest development cycle for a feature version release ever. We have completed **~150 issues**, merged **~150 PRs** and made **~1,000 commits** only in the main [abp repository](https://github.com/abpframework/abp). See the related [milestone](https://github.com/abpframework/abp/milestone/38?closed=1) on GitHub.
There were two main reasons of this long development cycle;
@ -66,7 +66,7 @@ If you already have a solution and want to use/test the latest RC/Preview versio
### ABP Commercial Only
* We've **moved favicons** into `/wwwroot/images/favicon/` folder for the ASP.NET Core **MVC / Razor Page UI** applications. There are 10 favicon related files (including the `favicon.ico`) under this directory to better work with different browser and cases. You can create a new application to check this folder and copy the files into your own application. Then you can customize the icons for your own brand.
* We've **moved favicons** into `/wwwroot/images/favicon/` folder for the ASP.NET Core **MVC / Razor Page UI** applications. There are 10 favicon related files (including the `favicon.ico`) under this directory to better work with different browser and cases. You can create a new application to check this folder and copy the files into your own application. Then you can customize the icons for your own brand (hint: you can use a tool [like that](https://realfavicongenerator.net/) to create the favicons with various formats).
* Removed direct **Twitter & Facebook social login integrations** from the [account module](https://commercial.abp.io/modules/Volo.Account.Pro), for **MVC / Razor Pages UI**. Follow [this documentation](https://github.com/abpframework/abp/blob/dev/docs/en/Authentication/Social-External-Logins.md) to easily add social logins to your applications if you need. The account module provides all the infrastructure to handle social/external logins, you just need to configure it.
## What's New with the ABP Framework 3.1 RC.1

405
docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/POST.md

@ -0,0 +1,405 @@
# Introducing the Angular Service Proxy Generation
Angular Service Proxy System **generates TypeScript services and models** to consume your backend HTTP APIs developed using the ABP Framework. So, you **don't manually create** models for your server side DTOs and perform raw HTTP calls to the server.
ABP Framework has introduced the **new** Angular Service Proxy Generation system with the version 3.1. While this feature was available since the [v2.3](https://blog.abp.io/abp/ABP-Framework-v2_3_0-Has-Been-Released), it was not well covering some scenarios, like inheritance and generic types and had some known problems. **With the v3.1, we've re-written** it using the [Angular Schematics](https://angular.io/guide/schematics) system. Now, it is much more stable and feature rich.
This post introduces the service proxy generation system and highlights some important features.
## Installation
### ABP CLI
You need to have the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to use the system. So, install it if you haven't installed before:
````bash
dotnet tool install -g Volo.Abp.Cli
````
If you already have installed it before, you can update to the latest version:
````shell
dotnet tool update -g Volo.Abp.Cli
````
### Project Configuration
> If you've created your project with version 3.1 or later, you can skip this part since it will be already installed in your solution.
For a solution that was created before v3.1, follow the steps below to configure the angular application:
* Add `@abp/ng.schematics` package to the `devDependencies` of the Angular project. Run the following command in the root folder of the angular application:
````bash
npm install @abp/ng.schematics --save-dev
````
- Add `rootNamespace` entry into the `apis/default` section in the `/src/environments/environment.ts`, as shown below:
```json
apis: {
default: {
...
rootNamespace: 'Acme.BookStore'
},
}
```
`Acme.BookStore` should be replaced by the root namespace of your .NET project. This ensures to not create unnecessary nested folders while creating the service proxy code. This value is `AngularProxyDemo` for the example solution explained below.
## Basic Usage
### Project Creation
> If you already have a solution, you can skip this section.
You need to [create](https://abp.io/get-started) your solution with the Angular UI. You can use the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) to create a new solution:
````bash
abp new AngularProxyDemo -u angular
````
#### Run the Application
The backend application must be up and running to be able to use the service proxy code generation system.
> See the [getting started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No) guide if you don't know details of creating and running the solution.
### Backend
Assume that we have an `IBookAppService` interface:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Books
{
public interface IBookAppService : IApplicationService
{
public Task<List<BookDto>> GetListAsync();
}
}
````
That uses a `BookDto` defined as shown:
```csharp
using System;
using Volo.Abp.Application.Dtos;
namespace AngularProxyDemo.Books
{
public class BookDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime PublishDate { get; set; }
}
}
```
And implemented as the following:
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Books
{
public class BookAppService : ApplicationService, IBookAppService
{
public async Task<List<BookDto>> GetListAsync()
{
//TODO: get books from a database...
}
}
}
```
It simply returns a list of books. You probably want to get the books from a database, but it doesn't matter for this article.
### HTTP API
Thanks to the [auto API controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) system of the ABP Framework, we don't have to develop API controllers manually. Just **run the backend (*HttpApi.Host*) application** that shows the [Swagger UI](https://swagger.io/tools/swagger-ui/) by default. You will see the **GET** API for the books:
![swagger-book-list](swagger-book-list.png)
### Service Proxy Generation
Open a **command line** in the **root folder of the Angular application** and execute the following command:
````bash
abp generate-proxy
````
It should produce an output like the following:
````bash
CREATE src/app/shared/models/books/index.ts (142 bytes)
CREATE src/app/shared/services/books/book.service.ts (437 bytes)
...
````
> `generate-proxy` command can take some some optional parameters for advanced scenarios (like [modular development](https://docs.abp.io/en/abp/latest/Module-Development-Basics)). You can take a look at the [documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Service-Proxies).
It basically creates two files;
src/app/shared/services/books/**book.service.ts**: This is the service that can be injected and used to get the list of books;
````typescript
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { BookDto } from '../../models/book';
@Injectable({
providedIn: 'root',
})
export class BookService {
apiName = 'Default';
getList = () =>
this.restService.request<any, BookDto[]>({
method: 'GET',
url: `/api/app/book`,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
````
src/app/shared/models/books/**index.ts**: This file contains the modal classes corresponding to the DTOs defined in the server side;
````typescript
import type { EntityDto } from '@abp/ng.core';
export interface BookDto extends EntityDto<string> {
name: string;
publishDate: string;
}
````
You can now inject the `BookService` into any Angular component and use the `getList()` method to get the list of books.
### About the Generated Code
The generated code is;
* **Simple**: It is almost identical to the code if you've written it yourself.
* **Splitted**: Instead of a single, large file;
* It creates a separate `.ts` file for every backend **service**. **Model** (DTO) classes are also grouped per service.
* It understands the [modularity](https://docs.abp.io/en/abp/latest/Module-Development-Basics), so creates the services for your own **module** (or the module you've specified).
* **Object oriented**;
* Supports **inheritance** of server side DTOs and generates the code respecting to the inheritance structure.
* Supports **generic types**.
* Supports **re-using type definitions** across services and doesn't generate the same DTO multiple times.
* **Well-aligned to the backend**;
* Service **method signatures** match exactly with the services on the backend services. This is achieved by a special endpoint exposed by the ABP Framework that well defines the backend contracts.
* **Namespaces** are exactly matches to the backend services and DTOs.
* **Well-aligned with the ABP Framework**;
* Recognizes the **standard ABP Framework DTO types** (like `EntityDto`, `ListResultDto`... etc) and doesn't repeat these classes in the application code, but uses from the `@abp/ng.core` package.
* Uses the `RestService` defined by the `@abp/ng.core` package which simplifies the generated code, keeps it short and re-uses all the logics implemented by the `RestService` (including error handling, authorization token injection, using multiple server endpoints... etc).
These are the main motivations behind the decision of creating a service proxy generation system, instead of using a pre-built tool like [NSWAG](https://github.com/RicoSuter/NSwag).
## Other Examples
Let me show you a few more examples.
### Updating an Entity
Assume that you added a new method to the server side application service, to update a book:
```csharp
public Task<BookDto> UpdateAsync(Guid id, BookUpdateDto input);
```
`BookUpdateDto` is a simple class defined shown below:
```csharp
using System;
namespace AngularProxyDemo.Books
{
public class BookUpdateDto
{
public string Name { get; set; }
public DateTime PublishDate { get; set; }
}
}
```
Let's re-run the `generate-proxy` command to see the result:
````
abp generate-proxy
````
The output of this command will be like the following:
````bash
UPDATE src/app/shared/services/books/book.service.ts (660 bytes)
UPDATE src/app/shared/models/books/index.ts (217 bytes)
````
It tells us two files have been updated. Let's see the changes;
**book.service.ts**
````typescript
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { BookDto, BookUpdateDto } from '../../models/books';
@Injectable({
providedIn: 'root',
})
export class BookService {
apiName = 'Default';
getList = () =>
this.restService.request<any, BookDto[]>({
method: 'GET',
url: `/api/app/book`,
},
{ apiName: this.apiName });
update = (id: string, input: BookUpdateDto) =>
this.restService.request<any, BookDto>({
method: 'PUT',
url: `/api/app/book/${id}`,
body: input,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
````
`update` function has been added to the `BookService` that gets an `id` and a `BookUpdateDto` as the parameters.
**index.ts**
````typescript
import type { EntityDto } from '@abp/ng.core';
export interface BookDto extends EntityDto<string> {
name: string;
publishDate: string;
}
export interface BookUpdateDto {
name: string;
publishDate: string;
}
````
Added a new DTO class: `BookUpdateDto`.
### Advanced Example
In this example, I want to show a DTO structure using inheritance, generics, arrays and dictionaries.
I've created an `IOrderAppService` as shown below:
````csharp
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace AngularProxyDemo.Orders
{
public interface IOrderAppService : IApplicationService
{
public Task CreateAsync(OrderCreateDto input);
}
}
````
`OrderCreateDto` and the related DTOs are as the followings;
````csharp
using System;
using System.Collections.Generic;
using Volo.Abp.Data;
namespace AngularProxyDemo.Orders
{
public class OrderCreateDto : IHasExtraProperties
{
public Guid CustomerId { get; set; }
public DateTime CreationTime { get; set; }
//ARRAY of DTOs
public OrderDetailDto[] Details { get; set; }
//DICTIONARY
public Dictionary<string, object> ExtraProperties { get; set; }
}
public class OrderDetailDto : GenericDetailDto<int> //INHERIT from GENERIC
{
public string Note { get; set; }
}
//GENERIC class
public abstract class GenericDetailDto<TCount>
{
public Guid ProductId { get; set; }
public TCount Count { get; set; }
}
}
````
When I run the `abp generate-proxy` command again, I see two new files have been created.
src/app/shared/services/orders/**order.service.ts**
````typescript
import { RestService } from '@abp/ng.core';
import { Injectable } from '@angular/core';
import type { OrderCreateDto } from '../../models/orders';
@Injectable({
providedIn: 'root',
})
export class OrderService {
apiName = 'Default';
create = (input: OrderCreateDto) =>
this.restService.request<any, void>({
method: 'POST',
url: `/api/app/order`,
body: input,
},
{ apiName: this.apiName });
constructor(private restService: RestService) {}
}
````
src/app/shared/models/orders/**index.ts**
````typescript
import type { GenericDetailDto } from '../../models/orders';
export interface OrderCreateDto {
customerId: string;
creationTime: string;
details: OrderDetailDto[];
extraProperties: string | object;
}
export interface OrderDetailDto extends GenericDetailDto<number> {
note: string;
}
````
NOTE: 3.1.0-rc2 was generating the code above, which is wrong. It will be fixed in the next RC versions.

BIN
docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/abp-generate-proxy-output.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/en/Blog-Posts/2020-09-07 Angular-Service-Proxies/swagger-book-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

46
docs/en/CLI.md

@ -16,6 +16,12 @@ To update an existing installation:
dotnet tool update -g Volo.Abp.Cli
````
## Global Options
While each command may have a set of options, there are some global options those can be used with any command;
* `--skip-cli-version-check`: Skips to check the latest version of the ABP CLI. If you don't specify, it will check the latest version and shows a warning message if there is a newer version of the ABP CLI.
## Commands
Here, the list of all available commands before explaining their details:
@ -25,7 +31,8 @@ Here, the list of all available commands before explaining their details:
* **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution.
* **`add-package`**: Adds an ABP package to a project.
* **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution.
* **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints on the server.
* **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints.
* **`remove-proxy`**: Removes previously generated client side proxies.
* **`switch-to-preview`**: Switches to the latest preview version of the ABP Framework.
* **`switch-to-nightly`**: Switches to the latest [nightly builds](Nightly-Builds.md) of the ABP related packages on a solution.
* **`switch-to-stable`**: Switches to the latest stable versions of the ABP related packages on a solution.
@ -173,26 +180,43 @@ abp add-module Volo.Blogging
### generate-proxy
Generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running `generate-proxy` command, your host must be up and running.
Generates Angular service proxies for your HTTP APIs to make easy to consume your services from the client side. Your host (server) application must be up and running before running this command.
Usage:
````bash
abp generate-proxy [options]
abp generate-proxy
````
#### Options
* `--apiUrl` or `-a`: Specifies the root URL of the HTTP API. The default value is being retrieved from the `environment.ts` file for the Angular application. Make sure your host is up and running before running `abp generate-proxy`.
* `--ui` or `-u`: Specifies the UI framework. Default value is `angular` and it is the only UI framework supported for now. Creates TypeScript code.
* `--module` or `-m`: Specifies the module name. Default module name is `app`, which indicates your own application (you typically want this since every module is responsible to maintain its own client proxies). Set `all` for to generate proxies for all the modules.
* `--module` or `-m`: Specifies the name of the backend module you wish to generate proxies for. Default value: `app`.
* `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
* `--target` or `-t`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
* `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.
### remove-proxy
Removes previously generated proxy code from the Angular application. Your host (server) application must be up and running before running this command.
Example usage with the options:
This can be especially useful when you generate proxies for multiple modules before and need to remove one of them later.
Usage:
````bash
abp generate-proxy --apiUrl https://localhost:44305 --ui angular --module all
abp remove-proxy
````
#### Options
* `--module` or `-m`: Specifies the name of the backend module you wish to remove proxies for. Default value: `app`.
* `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
* `--target` or `-t`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
* `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.
### switch-to-preview
@ -206,7 +230,7 @@ abp switch-to-preview [options]
#### Options
`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
* `--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
### switch-to-nightly
@ -221,7 +245,7 @@ abp switch-to-nightly [options]
#### Options
`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
* `--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
### switch-to-stable
@ -234,7 +258,7 @@ abp switch-to-stable [options]
````
#### Options
`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
* `--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
### translate

31
docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md

@ -2,15 +2,17 @@
## Introduction
To allow a user login with a magic URL, you need to implement a custom token provider. In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering a password.
In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering the password.
This can be useful especially if you want to make someone login to the application with your user, without sharing your secret password. The generated link will be for a single use.
### Source Code
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
## Creating the Solution
Before starting to the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No).
Before starting the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No).
## Step-1
@ -155,36 +157,33 @@ Open your **Index.cshtml** and set the content as below. We added a form that po
```html
@page
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L
@using Microsoft.AspNetCore.Mvc.Localization
@using PasswordlessAuthentication.Localization
@using PasswordlessAuthentication.Web.Menus
@using MyBookStore.Web.Menus
@using Volo.Abp.AspNetCore.Mvc.UI.Layout
@model PasswordlessAuthentication.Web.Pages.IndexModel
@model MyBookStore.Web.Pages.IndexModel
@using Microsoft.AspNetCore.Mvc.Localization
@using MyBookStore.Localization
@inject IHtmlLocalizer<MyBookStoreResource> L
@{
ViewBag.PageTitle = "Home";
}
@inject IPageLayout PageLayout;
@inject IPageLayout PageLayout
@{
PageLayout.Content.Title = L["Home"].Value;
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value);
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home;
PageLayout.Content.MenuItemName = MyBookStoreMenus.Home;
}
<abp-card>
<abp-card-body>
<form asp-page-handler="GeneratePasswordlessToken" method="post">
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button>
@if (Model.PasswordlessLoginUrl != null)
{
<abp-card class="mt-3 p-3">
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl)
<a href="@Model.PasswordlessLoginUrl">@Model.PasswordlessLoginUrl</a>
</abp-card>
}
</form>
</abp-card-body>
</abp-card>
@ -273,4 +272,4 @@ That's all! We created a passwordless login with 7 steps.
## Source Code
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).

18
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/Using-DevExtreme-In-ABP-Based-Application.md → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/Using-DevExtreme-Components-With-The-ABP-Framework.md

@ -1,9 +1,11 @@
## Using DevExtreme with ABP Based Applications
## Using DevExtreme Components With the ABP Framework
Hi, in this step by step article, I will show you how to integrate DevExtreme components into ABP Framework based applications.
Hi, in this step by step article, I will show you how to integrate [DevExtreme](https://js.devexpress.com/) components into ABP Framework-based applications.
![both-example-result](both-example-result.png)
*(A screenshot from the example application developed in this article)*
## Create the Project
ABP Framework offers startup templates to get into the business faster. We can download a new startup template using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI):
@ -151,7 +153,7 @@ namespace DevExtremeSample.Web.Bundling
}
```
As you see, the `DevExtremeScriptContributor` is depends on `JQueryScriptContributor` which adds JQuery related files before the DevExpress packages (see the [bundling system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification) for details).
As you see, the `DevExtremeScriptContributor` depends on `JQueryScriptContributor` which adds JQuery related files before the DevExpress packages (see the [bundling system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification) for details).
#### Create DevExtremeJsViewComponent
@ -184,7 +186,7 @@ namespace DevExtremeSample.Web.Components.DevExtremeJs
<abp-script type="typeof(DevExtremeScriptContributor)" />
```
Your final Web project should be like as the following:
Your final Web project should be like the following:
![devextreme-js](devextreme-js.png)
@ -204,7 +206,7 @@ Configure<AbpLayoutHookOptions>(options =>
#### Known Issue: Uncaught TypeError: MutationObserver.observe: Argument 1 is not an object.
> This issue does exists in the ABP Framework v3.0 and earlier versions. If you are using ABP Framework v3.1 or a latter version, you can skip this section.
> This issue does exist in the ABP Framework v3.0 and earlier versions. If you are using ABP Framework v3.1 or a later version, you can skip this section.
When you run your `*.Web` project, you will see an exception (`Uncaught TypeError: MutationObserver.observe: Argument 1 is not an object.`) at your console.
@ -265,7 +267,7 @@ I've used an in-memory list to store data for this example, instead of a real da
#### JSON Serialization
You can see some `JsonProperty` attributes on the DTO properties. I uses these attributes because DevExtreme example expects `PascalCase` property names in the serialized JSON that is sent to the client. But ABP Framework & ASP.NET Core conventionally uses `camelCase` property names on JSON serialization. Adding these `JsonProperty` attributes ensures that the related properties are serialized as `PascalCase`.
You can see some `JsonProperty` attributes on the DTO properties. I use these attributes because DevExtreme example expects `PascalCase` property names in the serialized JSON that is sent to the client. But ABP Framework & ASP.NET Core conventionally uses `camelCase` property names on JSON serialization. Adding these `JsonProperty` attributes ensures that the related properties are serialized as `PascalCase`.
#### DevExtreme Components vs Application Service Methods
@ -300,3 +302,7 @@ Html.DevExtreme().DataGrid<Order>()
.Key("OrderID")
)
```
## Conclusion
In this article, I've explained how to use [DevExtreme](https://js.devexpress.com/) components in your application. ABP Framework is designed so that it can work with any UI library/framework.

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/both-example-result.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/both-example-result.png

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/data-grid-app-contract.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-app-contract.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/data-grid-application.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-application.png

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/data-grid-final.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-final.png

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/data-grid-web.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/data-grid-web.png

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/devexp-result.gif → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/devexp-result.gif

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/devextreme-js.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/devextreme-js.png

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/gulp.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/gulp.png

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/initial-project.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/initial-project.png

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/tree-list-app-contract.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-app-contract.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/tree-list-application.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-application.png

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/tree-list-final.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-final.png

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/tree-list-web.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/tree-list-web.png

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
docs/en/Community-Articles/2020-08-18-DevExtreme-With-ABP/wwwroot-lib.png → docs/en/Community-Articles/2020-08-18-Using-DevExtreme-Components-With-The-ABP-Framework/wwwroot-lib.png

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

81
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md

@ -0,0 +1,81 @@
# ABP Suite: How to Add the User Entity as a Navigation Property of Another Entity
## Introduction
[ABP Suite](https://commercial.abp.io/tools/suite), a part of the [ABP Commercial](https://commercial.abp.io/), is a productivity tool developed by the team behind the ABP Framework. The main functionality of the ABP Suite is to generate code for you.
In this post, I'll show you how to add the user entity as a navigation property in your new entity, by the help of the ABP Suite.
> In the sample project MVC UI is used, but the same steps are applicable to the Angular UI as well.
## Code Generation
### Create a New Entity
Open the ABP Suite ([see how](https://docs.abp.io/en/commercial/latest/abp-suite/index)). Create a new entity called `Note`, as an example entity.
![create-note-entity](create-note-entity.jpg)
Then add a string property called `Title`, as an example property.
![add-simple-property](add-simple-property.jpg)
### Create AppUserDto
ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property.
To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`.
![create-appuserdto](create-appuserdto.jpg)
We should define the [object mapping](https://docs.abp.io/en/abp/latest/Object-To-Object-Mapping) to be able to convert the `AppUser` objects to `AppUserDto` objects. To do this, open `YourProjectApplicationAutoMapperProfile.cs` and add the below line:
```csharp
CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
```
![create-mapping](create-mapping.jpg)
> Creating such a DTO class may not be needed for another entity than the `AppUser`, since it will probably be already available, especially if you had created the other entity using the ABP Suite.
### Define the Navigation Property
Get back to ABP Suite, open the **Navigation Properties** tab of the ABP Suite, click the **Add Navigation Property** button. Browse `AppUser.cs` in `*.Domain\Users` folder. Then choose the `Name` item as display property. Browse `AppUserDto.cs` in `*.Contracts\Users` folder. Choose `Users` from Collection Names dropdown.
![add-user-navigation](add-user-navigation.jpg)
### Generate the Code!
That's it! Click **Save and generate** button to create your page. You'll see the following page if everything goes well.
![final-page](final-page.jpg)
This is the new page that has been created by the ABP Suite. It can perform the fundamental CRUD operations. Also, it has the "App user" column that shows the related user name (you can easily change the automatically created "App user" title from the **Entity Name** field of the navigation property creation screen).
**Picking Users from Look Up Table**
We used dropdown element to select a user from the user list. If you have a lot of users, then it's good to pick a user from a look up table. A look up table is a modal window that lets you filter data and pick one. To do this, get back to Suite and click **Edit** button of user navigation which is set as `AppUserId` name. Choose "Modal" from the "UI Pick Type" field. Then click **Save and generate** button to recreate your page.
![ui-pick-type-modal](ui-pick-type-modal.jpg)
After successful code generation, you'll see the the user can be picked from user table.
![ui-pick-type-modal2](ui-pick-type-modal2.jpg)
## About the ABP Commercial RC
This example has been implemented with **ABP Commercial 3.1.0-rc.3**. This is a RC version. If you want to install the CLI and Suite RC version follow the next steps:
1- Uninstall the current version of the CLI and install the specific RC version:
```bash
dotnet tool uninstall --global Volo.Abp.Cli && dotnet tool install --global Volo.Abp.Cli --version 3.1.0-rc.3
```
2- Uninstall the current version of the Suite and install the specific RC version:
```bash
dotnet tool uninstall --global Volo.Abp.Suite && dotnet tool install -g Volo.Abp.Suite --version 3.1.0-rc.3 --add-source https://nuget.abp.io/<YOUR-API-KEY>/v3/index.json
```
Don't forget to replace the `<YOUR-API-KEY>` with your own key!

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-simple-property.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-user-navigation.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-appuserdto.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-mapping.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-note-entity.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/final-page.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

2
docs/en/Customizing-Application-Modules-Overriding-Services.md

@ -61,7 +61,7 @@ In most cases, you will want to change one or a few methods of the current imple
````csharp
//[RemoteService(IsEnabled = false)] // If you use dynamic controller feature you can disable remote service. Prevent creating duplicate controller for the application service.
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService))]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService
{
//...

167
docs/en/Distributed-Event-Bus-Kafka-Integration.md

@ -0,0 +1,167 @@
# Distributed Event Bus Kafka Integration
> This document explains **how to configure the [Kafka](https://kafka.apache.org/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system
## Installation
Use the ABP CLI to add [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Kafka` package.
* Run `abp add-package Volo.Abp.EventBus.Kafka` command.
If you want to do it manually, install the [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusKafkaModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
## Configuration
You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes.
### `appsettings.json` file configuration
This is the simplest way to configure the Kafka settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/).
**Example: The minimal configuration to connect to a local kafka server with default configurations**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "localhost:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
* `MyGroupId` is the name of this application, which is used as the **GroupId** on the Kakfa.
* `MyTopicName` is the **topic name**.
See [the Kafka document](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html) to understand these options better.
#### Connections
If you need to connect to another server than the localhost, you need to configure the connection properties.
**Example: Specify the host name (as an IP address)**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus.
**Example: Declare two connections and use one of them for the event bus**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
},
"SecondConnection": {
"BootstrapServers": "321.321.321.321:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName",
"ConnectionName": "SecondConnection"
}
}
}
````
This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus.
You can use any of the [ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html) properties as the connection properties.
**Example: Specify the socket timeout**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092",
"SocketTimeoutMs": 60000
}
}
}
}
````
### The Options Classes
`AbpRabbitMqOptions` and `AbpRabbitMqEventBusOptions` classes can be used to configure the connection strings and event bus options for the RabbitMQ.
You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md).
**Example: Configure the connection**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.Connections.Default.BootstrapServers = "123.123.123.123:9092";
options.Connections.Default.SaslUsername = "user";
options.Connections.Default.SaslPassword = "pwd";
});
````
**Example: Configure the consumer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureConsumer = config =>
{
config.GroupId = "MyGroupId";
config.EnableAutoCommit = false;
};
});
````
**Example: Configure the producer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureProducer = config =>
{
config.MessageTimeoutMs = 6000;
config.Acks = Acks.All;
};
});
````
**Example: Configure the topic specification**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureTopic = specification =>
{
specification.ReplicationFactor = 3;
specification.NumPartitions = 3;
};
});
````
Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file.

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

@ -8,6 +8,7 @@ Distributed event bus system provides an **abstraction** that can be implemented
* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider.
* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it.
* `KafkaDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it.
Using a local event bus as default has a few important advantages. The most important one is that: It allows you to write your code compatible to distributed architecture. You can write a monolithic application now that can be split into microservices later. It is a good practice to communicate between bounded contexts (or between application modules) via distributed events instead of local events.

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

@ -376,6 +376,8 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
//Prepare a query to join books and authors
var query = from book in Repository
join author in _authorRepository on book.AuthorId equals author.Id
@ -397,6 +399,8 @@ namespace Acme.BookStore.Books
public override async Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Prepare a query to join books and authors
var query = from book in Repository
join author in _authorRepository on book.AuthorId equals author.Id
@ -495,6 +499,8 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
@ -507,6 +513,8 @@ namespace Acme.BookStore.Books
public override async Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
@ -915,6 +923,149 @@ You can run the application and try to create a new book or update an existing b
{{else if UI=="NG"}}
***Angular UI is being prepared...***
### The Book List
Book list page change is trivial. Open the `/src/app/book/book.component.html` and add the following column definition between the `Name` and `Type` columns:
````js
<ngx-datatable-column
[name]="'::Author' | abpLocalization"
prop="authorName"
></ngx-datatable-column>
````
When you run the application, you can see the *Author* column on the table:
![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular.png)
### Create/Edit Forms
The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below:
![bookstore-angular-author-selection](images/bookstore-angular-author-selection.png)
Added the Author dropdown as the first element in the form.
Open the `/src/app/book/book.component.ts` and and change the content as shown below:
````js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookService, BookDto, bookTypeOptions, AuthorLookupDto } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-book',
templateUrl: './book.component.html',
styleUrls: ['./book.component.scss'],
providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
form: FormGroup;
selectedBook = {} as BookDto;
authors$: Observable<AuthorLookupDto[]>;
bookTypes = bookTypeOptions;
isModalOpen = false;
constructor(
public readonly list: ListService,
private bookService: BookService,
private fb: FormBuilder,
private confirmation: ConfirmationService
) {
this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));
}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
});
}
createBook() {
this.selectedBook = {} as BookDto;
this.buildForm();
this.isModalOpen = true;
}
editBook(id: string) {
this.bookService.get(id).subscribe((book) => {
this.selectedBook = book;
this.buildForm();
this.isModalOpen = true;
});
}
buildForm() {
this.form = this.fb.group({
authorId: [this.selectedBook.authorId || null, Validators.required],
name: [this.selectedBook.name || null, Validators.required],
type: [this.selectedBook.type || null, Validators.required],
publishDate: [
this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
Validators.required,
],
price: [this.selectedBook.price || null, Validators.required],
});
}
save() {
if (this.form.invalid) {
return;
}
const request = this.selectedBook.id
? this.bookService.update(this.selectedBook.id, this.form.value)
: this.bookService.create(this.form.value);
request.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
}
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
}
}
````
* Added imports for the `AuthorLookupDto`, `Observable` and `map`.
* Added `authors$: Observable<AuthorLookupDto[]>;` field after the `selectedBook`.
* Added `this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items));` into the constructor.
* Added ` authorId: [this.selectedBook.authorId || null, Validators.required],` into the `buildForm()` function.
Open the `/src/app/book/book.component.html` and add the following form group just before the book name form group:
````html
<div class="form-group">
<label for="author-id">Author</label><span> * </span>
<select class="form-control" id="author-id" formControlName="authorId">
<option [ngValue]="null">Select author</option>
<option [ngValue]="author.id" *ngFor="let author of authors$ | async">
{%{{{ author.name }}}%}
</option>
</select>
</div>
````
That's all. Just run the application and try to create or edit an author.
{{end}}

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

@ -347,9 +347,9 @@ This is a fully working, server side paged, sorted and localized table of books.
## Install NPM packages
> Notice: This tutorial is based on the ABP Framework v3.0.3+ If your project version is older, then please upgrade your solution. See the [migration guide](../UI/Angular/Migration-Guide-v3.md) if you are upgrading an existing project with v2.x.
> Notice: This tutorial is based on the ABP Framework v3.1.0+ If your project version is older, then please upgrade your solution. See the [migration guide](../UI/Angular/Migration-Guide-v3.md) if you are upgrading an existing project with v2.x.
If you haven't done it before, open a new command line interface (terminal window) and go to your `angular` folder and then run `yarn` command to install NPM packages:
If you haven't done it before, open a new command line interface (terminal window) and go to your `angular` folder and then run `yarn` command to install the NPM packages:
```bash
yarn
@ -473,9 +473,9 @@ Run the following command in the `angular` folder:
abp generate-proxy
```
The generated files looks like below:
This command will create the following files under the `/src/app/proxy/books` folder:
![Generated files](./images/generated-proxies-2.png)
![Generated files](./images/generated-proxies-3.png)
### BookComponent
@ -484,8 +484,7 @@ Open the `/src/app/book/book.component.ts` file and replace the content as below
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto } from './models';
import { BookService } from './services';
import { BookService, BookDto } from '@proxy/books';
@Component({
selector: 'app-book',
@ -499,7 +498,7 @@ export class BookComponent implements OnInit {
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;

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

@ -656,8 +656,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto } from './models';
import { BookService } from './services';
import { BookService, BookDto } from '@proxy/books';
@Component({
selector: 'app-book',
@ -673,7 +672,7 @@ export class BookComponent implements OnInit {
constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@ -749,8 +748,8 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from './models'; // add BookType
import { BookService } from './services';
// import bookTypeOptions from @proxy/books
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this
@Component({
@ -764,12 +763,8 @@ export class BookComponent implements OnInit {
form: FormGroup; // add this line
bookType = BookType; // add this line
// add bookTypes as a list of BookType enum members
bookTypes = Object.keys(this.bookType).filter(
(key) => typeof this.bookType[key] === 'number'
);
bookTypes = bookTypeOptions;
isModalOpen = false;
@ -780,7 +775,7 @@ export class BookComponent implements OnInit {
) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@ -808,7 +803,7 @@ export class BookComponent implements OnInit {
return;
}
this.bookService.createByInput(this.form.value).subscribe(() => {
this.bookService.create(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
@ -819,7 +814,6 @@ export class BookComponent implements OnInit {
* Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`.
* Added `form: FormGroup` property.
* Added `bookType` property so that you can reach `BookType` enum members from template.
* Added `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
* Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms.
* Added `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
@ -844,7 +838,7 @@ Open `/src/app/book/book.component.html` and replace `<ng-template #abpBody> </n
<label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option>
<option [ngValue]="type.value" *ngFor="let type of bookTypes"> {%{{{ type.key }}}%}</option>
</select>
</div>
@ -910,8 +904,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from './models';
import { BookService } from './services';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
// added this line
@ -931,11 +924,7 @@ export class BookComponent implements OnInit {
form: FormGroup;
bookType = BookType;
bookTypes = Object.keys(this.bookType).filter(
(key) => typeof this.bookType[key] === 'number'
);
bookTypes = bookTypeOptions;
isModalOpen = false;
@ -946,7 +935,7 @@ export class BookComponent implements OnInit {
) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@ -972,7 +961,7 @@ export class BookComponent implements OnInit {
return;
}
this.bookService.createByInput(this.form.value).subscribe(() => {
this.bookService.create(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
@ -995,8 +984,7 @@ Open `/src/app/book/book.component.ts` and replace the content as shown below:
```js
import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType, CreateUpdateBookDto } from './models';
import { BookService } from './services';
import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -1009,15 +997,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
selectedBook = new BookDto(); // declare selectedBook
selectedBook = {} as BookDto; // declare selectedBook
form: FormGroup;
bookType = BookType;
bookTypes = Object.keys(this.bookType).filter(
(key) => typeof this.bookType[key] === 'number'
);
bookTypes = bookTypeOptions;
isModalOpen = false;
@ -1028,7 +1012,7 @@ export class BookComponent implements OnInit {
) {}
ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query);
const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response;
@ -1036,14 +1020,14 @@ export class BookComponent implements OnInit {
}
createBook() {
this.selectedBook = new BookDto(); // reset the selected book
this.selectedBook = {} as BookDto; // reset the selected book
this.buildForm();
this.isModalOpen = true;
}
// Add editBook method
editBook(id: string) {
this.bookService.getById(id).subscribe((book) => {
this.bookService.get(id).subscribe((book) => {
this.selectedBook = book;
this.buildForm();
this.isModalOpen = true;
@ -1069,8 +1053,8 @@ export class BookComponent implements OnInit {
}
const request = this.selectedBook.id
? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id)
: this.bookService.createByInput(this.form.value);
? this.bookService.update(this.selectedBook.id, this.form.value)
: this.bookService.create(this.form.value);
request.subscribe(() => {
this.isModalOpen = false;
@ -1155,7 +1139,7 @@ constructor(
delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.bookService.deleteById(id).subscribe(() => this.list.get());
this.bookService.delete(id).subscribe(() => this.list.get());
}
});
}

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

@ -621,7 +621,7 @@ abp generate-proxy
This command generates the service proxy for the author service and the related model (DTO) classes:
![bookstore-angular-service-proxy-author](images/bookstore-angular-service-proxy-author.png)
![bookstore-angular-service-proxy-author](images/bookstore-angular-service-proxy-author-2.png)
### AuthorComponent
@ -630,8 +630,7 @@ Open the `/src/app/author/author.component.ts` file and replace the content as b
```js
import { Component, OnInit } from '@angular/core';
import { ListService, PagedResultDto } from '@abp/ng.core';
import { AuthorDto } from './models';
import { AuthorService } from './services';
import { AuthorService, AuthorDto } from '@proxy/authors';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
@ -649,7 +648,7 @@ export class AuthorComponent implements OnInit {
form: FormGroup;
selectedAuthor = new AuthorDto();
selectedAuthor = {} as AuthorDto;
constructor(
public readonly list: ListService,
@ -659,7 +658,7 @@ export class AuthorComponent implements OnInit {
) {}
ngOnInit(): void {
const authorStreamCreator = (query) => this.authorService.getListByInput(query);
const authorStreamCreator = (query) => this.authorService.getList(query);
this.list.hookToQuery(authorStreamCreator).subscribe((response) => {
this.author = response;
@ -667,13 +666,13 @@ export class AuthorComponent implements OnInit {
}
createAuthor() {
this.selectedAuthor = new AuthorDto();
this.selectedAuthor = {} as AuthorDto;
this.buildForm();
this.isModalOpen = true;
}
editAuthor(id: string) {
this.authorService.getById(id).subscribe((author) => {
this.authorService.get(id).subscribe((author) => {
this.selectedAuthor = author;
this.buildForm();
this.isModalOpen = true;
@ -697,14 +696,14 @@ export class AuthorComponent implements OnInit {
if (this.selectedAuthor.id) {
this.authorService
.updateByIdAndInput(this.form.value, this.selectedAuthor.id)
.update(this.selectedAuthor.id, this.form.value)
.subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
});
} else {
this.authorService.createByInput(this.form.value).subscribe(() => {
this.authorService.create(this.form.value).subscribe(() => {
this.isModalOpen = false;
this.form.reset();
this.list.get();
@ -716,7 +715,7 @@ export class AuthorComponent implements OnInit {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure')
.subscribe((status) => {
if (status === Confirmation.Status.confirm) {
this.authorService.deleteById(id).subscribe(() => this.list.get());
this.authorService.delete(id).subscribe(() => this.list.get());
}
});
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/en/Tutorials/images/generated-proxies-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

109
docs/en/UI/Angular/Environment.md

@ -0,0 +1,109 @@
# Environment
Every application needs some ** environment ** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for ABP as well.
Current `Environment` configuration holds sub config classes as follows:
```typescript
export interface Environment {
apis: Apis;
application: Application;
oAuthConfig: AuthConfig;
production: boolean;
remoteEnv?: RemoteEnv;
}
```
## Apis
```typescript
export interface Apis {
[key: string]: ApiConfig;
default: ApiConfig;
}
export interface ApiConfig {
[key: string]: string;
rootNamespace?: string;
url: string;
}
```
Api config has to have a default config and it may have some additional ones for different modules.
I.e. you may want to connect to different Apis for different modules.
Take a look at following example
```json
{
// ...
"apis": {
"default": {
"url": "https://localhost:8080",
},
"AbpIdentity": {
"url": "https://localhost:9090",
}
},
// ...
}
```
When an api from `AbpIdentity` is called, the request will be sent to `"https://localhost:9090"`.
Everything else will be sent to `"https://localhost:8080"`
* `rootNamespace` **(new)** : Root namespace of the related API. e.g. Acme.BookStore
## Application
```typescript
export interface Application {
name: string;
baseUrl?: string;
logoUrl?: string;
}
```
* `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided.
* `logoUrl`: Url of the application logo. It is used by `logo.component`
* `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver)
## AuthConfig
For authentication, we use angular-oauth2-oidc. Please check their [docs](https://github.com/manfredsteyer/angular-oauth2-oidc) out
## RemoteEnvironment
Some applications need to integrate an existing config into the `environment` used throughout the application.
Abp Framework supports this out of box.
To integrate an existing config json into the `environment`, you need to set `remoteEnv`
```typescript
export type customMergeFn = (
localEnv: Partial<Config.Environment>,
remoteEnv: any,
) => Config.Environment;
export interface RemoteEnv {
url: string;
mergeStrategy: 'deepmerge' | 'overwrite' | customMergeFn;
method?: string;
headers?: ABP.Dictionary<string>;
}
```
* `url` *: Required. The url to be used to retrieve environment config
* `mergeStrategy` *: Required. Defines how the local and the remote `environment` json will be merged
* `deepmerge`: Both local and remote `environment` json will be merged recursively. If both configs have same nested path, the remote `environment` will be prioritized.
* `overwrite`: Remote `environment` will be used and local environment will be ignored.
* `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial<Config.Environment>` and `remoteEnv` and it needs to return a `Config.Environment` object.
* `method`: HTTP method to be used when retrieving environment config. Default: `GET`
* `headers`: If extra headers are needed for the request, it can be set through this field.
## What's Next?
* [Service Proxies](./Service-Proxies.md)

2
docs/en/UI/Angular/Migration-Guide-v3.md

@ -473,5 +473,5 @@ Some interfaces have long been marked as deprecated and now they are removed.
## What's Next?
* [Service Proxies](./Service-Proxies.md)
* [Environment](./Environment.md)

12
docs/en/docs-nav.json

@ -198,6 +198,10 @@
{
"text": "RabbitMQ Integration",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
},
{
"text": "Kafka Integration",
"path": "Distributed-Event-Bus-Kafka-Integration.md"
}
]
}
@ -219,11 +223,11 @@
"items": [
{
"text": "Email Sending System",
"path": "Emailing.md",
"path": "Emailing.md"
},
{
"text": "MailKit Integration",
"path": "MailKit.md",
"path": "MailKit.md"
}
]
},
@ -423,6 +427,10 @@
"text": "Migration Guide v2.x to v3",
"path": "UI/Angular/Migration-Guide-v3.md"
},
{
"text": "Environment",
"path": "UI/Angular/Environment.md"
},
{
"text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md"

2
docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md

@ -61,7 +61,7 @@ context.Services.Replace(
````csharp
//[RemoteService(IsEnabled = false)] // 如果你在使用动态控制器,为了避免为应用服务创建重复的控制器, 你可以禁用远程访问.
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService))]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService
{
//...

163
docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md

@ -0,0 +1,163 @@
# 分布式事件总线Kafka集成
> 本文解释了**如何配置[Kafka](https://kafka.apache.org/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.Kafka[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.Kafka` 包的 `.csproj` 文件目录打开命令行(终端).
* 运行 `abp add-package Volo.Abp.EventBus.Kafka` 命令.
如果你想要手动安装,安装[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusKafkaModule))]` 到你的项目[模块](Module-Development-Basics.md)类.
## 配置
可以使用配置使用标准的[配置系统](Configuration.md),如 `appsettings.json` 文件,或[选项](Options.md)类.
### `appsettings.json` 文件配置
这是配置Kafka设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持的](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
**示例:最小化配置与默认配置连接到本地的Kafka服务器**
````json
{
"Kafka": {
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
* `MyGroupId` 是应用程序的名称,用于Kafka的**GroupId**.
* `MyTopicName` 是**topic名称**.
参阅[Kafka文档](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html)更好的了解这些选项.
#### 连接
如果需要连接到本地主机以外的另一台服务器,需要配置连接属性.
**示例: 指定主机名 (如IP地址)**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
允许定义多个连接. 在这种情况下,你可以指定用于事件总线的连接.
**示例: 声明两个连接并将其中一个用于事件总线**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
},
"SecondConnection": {
"BootstrapServers": "321.321.321.321:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName",
"ConnectionName": "SecondConnection"
}
}
}
````
这允许你可以在你的应用程序使用多个Kafka服务器,但将其中一个做为事件总线.
你可以使用任何[ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html)属性作为连接属性.
**示例: 指定socket超时时间**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092",
"SocketTimeoutMs": 60000
}
}
}
}
````
### 选项类
`AbpKafkaOptions` 和 `AbpKafkaEventBusOptions` 类用于配置Kafka的连接字符串和事件总线选项.
你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项.
**示例: 配置连接**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.Connections.Default.BootstrapServers = "123.123.123.123:9092";
options.Connections.Default.SaslUsername = "user";
options.Connections.Default.SaslPassword = "pwd";
});
````
**示例: 配置 consumer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureConsumer = config =>
{
config.GroupId = "MyGroupId";
config.EnableAutoCommit = false;
};
});
````
**示例: 配置 producer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureProducer = config =>
{
config.MessageTimeoutMs = 6000;
config.Acks = Acks.All;
};
});
````
**示例: 配置 topic specification**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureTopic = specification =>
{
specification.ReplicationFactor = 3;
specification.NumPartitions = 3;
};
});
````
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.

1
docs/zh-Hans/Distributed-Event-Bus.md

@ -8,6 +8,7 @@
* `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**.
* `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-Integration.md)了解如何配置它.
* `KafkaDistributedEventBus` 通过[Kafka](https://kafka.apache.org/)实现分布式事件总线. 请参阅[Kafka集成文档](Distributed-Event-Bus-Kafka-Integration.md)了解如何配置它.
使用本地事件总线作为默认具有一些重要的优点. 最重要的是:它允许你编写与分布式体系结构兼容的代码. 您现在可以编写一个整体应用程序,以后可以拆分成微服务. 最好通过分布式事件而不是本地事件在边界上下文之间(或在应用程序模块之间)进行通信.

4
docs/zh-Hans/docs-nav.json

@ -180,6 +180,10 @@
{
"text": "RabbitMQ 集成",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
},
{
"text": "Kafka 集成",
"path": "Distributed-Event-Bus-Kafka-Integration.md"
}
]
}

14
framework/Volo.Abp.sln

@ -323,6 +323,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws.Tests", "test\Volo.Abp.BlobStoring.Aws.Tests\Volo.Abp.BlobStoring.Aws.Tests.csproj", "{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Kafka", "src\Volo.Abp.Kafka\Volo.Abp.Kafka.csproj", "{2A864049-9CD5-4493-8CDB-C408474D43D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Kafka", "src\Volo.Abp.EventBus.Kafka\Volo.Abp.EventBus.Kafka.csproj", "{C1D891B0-AE83-42CB-987D-425A2787DE78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.GlobalFeatures", "src\Volo.Abp.GlobalFeatures\Volo.Abp.GlobalFeatures.csproj", "{04F44063-C952-403A-815F-EFB778BDA125}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.GlobalFeatures.Tests", "test\Volo.Abp.GlobalFeatures.Tests\Volo.Abp.GlobalFeatures.Tests.csproj", "{231F1581-AA21-44C3-BF27-51EB3AD5355C}"
@ -965,6 +969,14 @@ Global
{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Release|Any CPU.Build.0 = Release|Any CPU
{2A864049-9CD5-4493-8CDB-C408474D43D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A864049-9CD5-4493-8CDB-C408474D43D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A864049-9CD5-4493-8CDB-C408474D43D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A864049-9CD5-4493-8CDB-C408474D43D4}.Release|Any CPU.Build.0 = Release|Any CPU
{C1D891B0-AE83-42CB-987D-425A2787DE78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1D891B0-AE83-42CB-987D-425A2787DE78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1D891B0-AE83-42CB-987D-425A2787DE78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1D891B0-AE83-42CB-987D-425A2787DE78}.Release|Any CPU.Build.0 = Release|Any CPU
{04F44063-C952-403A-815F-EFB778BDA125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04F44063-C952-403A-815F-EFB778BDA125}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04F44063-C952-403A-815F-EFB778BDA125}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1136,6 +1148,8 @@ Global
{8E49687A-E69F-49F2-8DB0-428D0883A937} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{50968CDE-1029-4051-B2E5-B69D0ECF2A18} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{2CD3B26A-CA81-4279-8D5D-6A594517BB3F} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{2A864049-9CD5-4493-8CDB-C408474D43D4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{C1D891B0-AE83-42CB-987D-425A2787DE78} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{04F44063-C952-403A-815F-EFB778BDA125} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{231F1581-AA21-44C3-BF27-51EB3AD5355C} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs

@ -0,0 +1,13 @@
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.CropperJs
{
public class CropperJsScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/cropperjs/js/cropper.min.js");
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.CropperJs
{
public class CropperJsStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/cropperjs/css/cropper.min.css");
}
}
}

1
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs

@ -8,6 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Luxon
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/luxon/luxon.min.js");
context.Files.AddIfNotContains("/libs/abp/luxon/abp.luxon.js");
}
}
}

16
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/StarRatingSvg/StarRatingSvgScriptContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg
{
[DependsOn(typeof(JQueryScriptContributor))]
public class StarRatingSvgScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/star-rating-svg/js/jquery.star-rating-svg.min.js");
}
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/StarRatingSvg/StarRatingSvgStyleContributor.cs

@ -0,0 +1,13 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg
{
public class StarRatingSvgStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/star-rating-svg/css/star-rating-svg.css");
}
}
}

4
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs

@ -20,6 +20,7 @@ namespace Volo.Abp.Cli
Configure<AbpCliOptions>(options =>
{
//TODO: Define constants like done for GenerateProxyCommand.Name.
options.Commands["help"] = typeof(HelpCommand);
options.Commands["new"] = typeof(NewCommand);
options.Commands["get-source"] = typeof(GetSourceCommand);
@ -28,7 +29,8 @@ namespace Volo.Abp.Cli
options.Commands["add-module"] = typeof(AddModuleCommand);
options.Commands["login"] = typeof(LoginCommand);
options.Commands["logout"] = typeof(LogoutCommand);
options.Commands["generate-proxy"] = typeof(GenerateProxyCommand);
options.Commands[GenerateProxyCommand.Name] = typeof(GenerateProxyCommand);
options.Commands[RemoveProxyCommand.Name] = typeof(RemoveProxyCommand);
options.Commands["suite"] = typeof(SuiteCommand);
options.Commands["switch-to-preview"] = typeof(SwitchToPreviewCommand);
options.Commands["switch-to-stable"] = typeof(SwitchToStableCommand);

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs

@ -41,9 +41,13 @@ namespace Volo.Abp.Cli
{
Logger.LogInformation("ABP CLI (https://abp.io)");
await CheckCliVersionAsync();
var commandLineArgs = CommandLineArgumentParser.Parse(args);
if (!commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
{
await CheckCliVersionAsync();
}
var commandType = CommandSelector.Select(commandLineArgs);
using (var scope = ServiceScopeFactory.CreateScope())
@ -72,7 +76,7 @@ namespace Volo.Abp.Cli
var currentCliVersion = await GetCurrentCliVersion(assembly);
var updateChannel = GetUpdateChannel(currentCliVersion);
Logger.LogInformation($"Version {currentCliVersion} ({updateChannel} channel)");
Logger.LogInformation($"Version {currentCliVersion} ({updateChannel})");
try
{
@ -190,8 +194,7 @@ namespace Volo.Abp.Cli
break;
case UpdateChannel.Prerelease:
Logger.LogWarning($"dotnet tool uninstall {toolPathArg} Volo.Abp.Cli");
Logger.LogWarning($"dotnet tool install {toolPathArg} Volo.Abp.Cli --version {latestVersion}");
Logger.LogWarning($"dotnet tool update {toolPathArg} Volo.Abp.Cli --version {latestVersion}");
break;
case UpdateChannel.Nightly:

1
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs

@ -115,7 +115,6 @@ namespace Volo.Abp.Cli.Commands
throw new CliUsageException(sb.ToString());
}
public static class Options
{
public static class Project

832
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -1,835 +1,11 @@
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.DependencyInjection;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using Volo.Abp.Localization;
using Volo.Abp.Reflection;
namespace Volo.Abp.Cli.Commands
{
public class GenerateProxyCommand : IConsoleCommand, ITransientDependency
{
public static Dictionary<string, Dictionary<string, string>> propertyList = new Dictionary<string, Dictionary<string, string>>();
public ILogger<GenerateProxyCommand> Logger { get; set; }
public static string outputPrefix = "src/app";
public static string output = "";
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
public GenerateProxyCommand(TemplateProjectBuilder templateProjectBuilder)
{
TemplateProjectBuilder = templateProjectBuilder;
Logger = NullLogger<GenerateProxyCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
var angularPath = $"angular.json";
if (!File.Exists(angularPath))
{
throw new CliUsageException(
"angular.json file not found. You must run this command in angular folder." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long);
module = module == null ? "app" : module.ToLower();
var apiUrl = commandLineArgs.Options.GetOrNull(Options.ApiUrl.Short, Options.ApiUrl.Long);
if (string.IsNullOrWhiteSpace(apiUrl))
{
var environmentJson = File.ReadAllText("src/environments/environment.ts").Split("export const environment = ")[1].Replace(";", " ");
var environment = JObject.Parse(environmentJson);
apiUrl = environment["apis"]["default"]["url"].ToString();
}
apiUrl = apiUrl.EnsureEndsWith('/') + "api/abp/api-definition?IncludeTypes=true";
var uiFramework = GetUiFramework(commandLineArgs);
output = commandLineArgs.Options.GetOrNull(Options.Output.Short, Options.Output.Long);
if (!string.IsNullOrWhiteSpace(output) && !(output.EndsWith("/") || output.EndsWith("\\")))
{
output += "/";
}
WebClient client = new WebClient();
string json = "";
try
{
json = client.DownloadString(apiUrl);
}
catch (Exception ex)
{
throw new CliUsageException(
"Cannot connect to the host {" + apiUrl + "}! Check that the host is up and running." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
Logger.LogInformation("Downloading api definition...");
Logger.LogInformation("Api Url: " + apiUrl);
var data = JObject.Parse(json);
Logger.LogInformation("Modules are combining");
var apiNameList = new Dictionary<string, string>();
var moduleList = GetCombinedModules(data, module, out apiNameList);
if (moduleList.Count < 1)
{
throw new CliUsageException(
"Module can not find!" +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
Logger.LogInformation("Modules and types are creating");
foreach (var moduleItem in moduleList)
{
var moduleValue = JObject.Parse(moduleItem.Value);
var rootPath = moduleItem.Key;
var apiName = apiNameList.Where(p => p.Key == rootPath).Select(p => p.Value).FirstOrDefault();
if (rootPath != "app")
{
Logger.LogInformation($"{rootPath} directory is creating");
}
if (rootPath == "app")
{
outputPrefix = "src";
}
else
{
outputPrefix = "src/app";
}
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}");
foreach (var controller in moduleValue.Root.ToList().Select(item => item.First))
{
var serviceIndexList = new List<string>();
var modelIndexList = new List<string>();
var serviceFileText = new StringBuilder();
serviceFileText.AppendLine("[firstTypeList]");
serviceFileText.AppendLine("import { Injectable } from '@angular/core';");
serviceFileText.AppendLine("import { Observable } from 'rxjs';");
serviceFileText.AppendLine("[secondTypeList]");
serviceFileText.AppendLine("");
serviceFileText.AppendLine("@Injectable({providedIn: 'root'})");
serviceFileText.AppendLine("export class [controllerName]Service {");
serviceFileText.AppendLine(" apiName = '" + apiName + "';");
serviceFileText.AppendLine("");
serviceFileText.AppendLine(" constructor(private restService: RestService) {}");
serviceFileText.AppendLine("");
var firstTypeList = new List<string>();
var secondTypeList = new List<string>();
var controllerName = (string)controller["controllerName"];
var controllerServiceName = controllerName.PascalToKebabCase() + ".service.ts";
var controllerPathName = controllerName.ToLower().Replace("controller", "");
controllerPathName = (controllerPathName.StartsWith(rootPath)) ? controllerPathName.Substring(rootPath.Length) : controllerPathName;
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models");
Directory.CreateDirectory(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services");
foreach (var actionItem in controller["actions"])
{
var action = actionItem.First;
var actionName = (string)action["uniqueName"];
actionName = (char.ToLower(actionName[0]) + actionName.Substring(1)).Replace("Async", "").Replace("Controller", "");
var returnValueType = (string)action["returnValue"]["type"];
var parameters = action["parameters"];
var parametersText = new StringBuilder();
var parametersIndex = 0;
var bodyExtra = "";
var modelBindingExtra = "";
var modelBindingExtraList = new List<string>();
var parameterModel = new List<ParameterModel>();
foreach (var parameter in parameters.OrderBy(p => p["bindingSourceId"]))
{
var bindingSourceId = (string)parameter["bindingSourceId"];
bindingSourceId = char.ToLower(bindingSourceId[0]) + bindingSourceId.Substring(1);
var name = (string)parameter["name"];
var typeSimple = (string)parameter["typeSimple"];
var typeArray = ((string)parameter["type"]).Split(".");
var type = (typeArray[typeArray.Length - 1]).TrimEnd('>');
var isOptional = (bool)parameter["isOptional"];
var defaultValue = (string)parameter["defaultValue"];
var modelIndex = CreateType(data, (string)parameter["type"], rootPath, modelIndexList, controllerPathName);
if (!string.IsNullOrWhiteSpace(modelIndex))
{
modelIndexList.Add(modelIndex);
}
if (bindingSourceId == "body")
{
bodyExtra = ", body";
parameterModel = AddParameter(bindingSourceId, type, isOptional, defaultValue, bindingSourceId, parameterModel);
}
else if (bindingSourceId == "path")
{
parameterModel = AddParameter(name, typeSimple, isOptional, defaultValue, bindingSourceId, parameterModel);
}
else if (bindingSourceId == "modelBinding")
{
var parameterNameOnMethod = (string)parameter["nameOnMethod"];
var parametersOnMethod = action["parametersOnMethod"];
foreach (var parameterOnMethod in parametersOnMethod)
{
var parametersOnMethodName = (string)parameterOnMethod["name"];
if (parametersOnMethodName == parameterNameOnMethod)
{
typeSimple = (string)parameterOnMethod["typeSimple"];
typeArray = ((string)parameterOnMethod["type"]).Split(".");
type = typeArray[typeArray.Length - 1];
isOptional = (bool)parameterOnMethod["isOptional"];
defaultValue = (string)parameterOnMethod["defaultValue"];
if (typeSimple == "string" || typeSimple == "boolean" || typeSimple == "number")
{
parameterModel = AddParameter(name, typeSimple, isOptional, defaultValue, bindingSourceId, parameterModel);
}
modelIndex = CreateType(data, (string)parameterOnMethod["type"], rootPath, modelIndexList, controllerPathName);
if (!string.IsNullOrWhiteSpace(modelIndex))
{
modelIndexList.Add(modelIndex);
}
}
}
if (typeSimple != "string" && typeSimple != "boolean" && typeSimple != "number")
{
parametersText.Append($"params = {{}} as {type}");
modelBindingExtra = ", params";
if (!string.IsNullOrWhiteSpace(modelIndex))
{
secondTypeList.Add(type);
}
else
{
firstTypeList.Add(type);
}
break;
}
}
}
if (parameterModel != null && parameterModel.Count > 0)
{
foreach (var parameterItem in parameterModel.OrderBy(p => p.DisplayOrder))
{
var parameterItemModelName = parameterItem.Type.PascalToKebabCase() + ".ts";
var parameterItemModelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{parameterItemModelName}";
if (parameterItem.BindingSourceId == "body" && !File.Exists(parameterItemModelPath))
{
parameterItem.Type = "any";
}
parametersIndex++;
if (parametersIndex > 1)
{
parametersText.Append(", ");
}
parametersText.Append(parameterItem.Name + (parameterItem.IsOptional ? "?" : "") + ": " + parameterItem.Type + (parameterItem.Value != null ? (" = " + (string.IsNullOrWhiteSpace(parameterItem.Value) ? "''" : parameterItem.Value)) : ""));
if (parameterItem.BindingSourceId == "modelBinding")
{
modelBindingExtraList.Add(parameterItem.Name);
}
else if (parameterItem.BindingSourceId == "body" && File.Exists(parameterItemModelPath))
{
secondTypeList.Add(parameterItem.Type);
}
}
}
if (returnValueType != null)
{
if (returnValueType.IndexOf('<') > -1)
{
var firstTypeArray = returnValueType.Split("<")[0].Split(".");
var firstType = firstTypeArray[firstTypeArray.Length - 1];
var secondTypeArray = returnValueType.Split("<")[1].Split(".");
var secondType = secondTypeArray[secondTypeArray.Length - 1].TrimEnd('>');
var secondTypeModelName = secondType.PascalToKebabCase() + ".ts";
var secondTypeModelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{secondTypeModelName}";
if (firstType == "List" && !File.Exists(secondTypeModelPath))
{
secondType = "any";
}
serviceFileText.AppendLine(
firstType == "List"
? $" {actionName}({parametersText}): Observable<{secondType}[]> {{"
: $" {actionName}({parametersText}): Observable<{firstType}<{secondType}>> {{");
if (firstType != "List")
{
firstTypeList.Add(firstType);
}
if (secondType != "any")
{
secondTypeList.Add(secondType);
}
}
else
{
var typeArray = returnValueType.Split(".");
var type = typeArray[typeArray.Length - 1].TrimEnd('>');
type = type switch
{
"Void" => "void",
"String" => "string",
"IActionResult" => "void",
"ActionResult" => "void",
"Int64" => "number",
"Int32" => "number",
_ => type
};
serviceFileText.AppendLine(
$" {actionName}({parametersText}): Observable<{type}> {{");
if (type != "void" && type != "string" && type != "number")
{
secondTypeList.Add(type);
}
}
var modelIndex = CreateType(data, returnValueType, rootPath, modelIndexList, controllerPathName);
if (!string.IsNullOrWhiteSpace(modelIndex))
{
modelIndexList.Add(modelIndex);
}
}
if (modelBindingExtraList != null && modelBindingExtraList.Count > 0)
{
modelBindingExtra = ", params: { " + string.Join(", ", modelBindingExtraList.ToArray()) + " }";
}
var url = ((string)action["url"]).Replace("/{", "/${");
var httpMethod = (string)action["httpMethod"];
serviceFileText.AppendLine(
url.Contains("${")
? $" return this.restService.request({{ url: `/{url}`, method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});"
: $" return this.restService.request({{ url: '/{url}', method: '{httpMethod}'{bodyExtra}{modelBindingExtra} }},{{ apiName: this.apiName }});");
serviceFileText.AppendLine(" }");
}
serviceIndexList.Add(controllerServiceName.Replace(".ts", ""));
if (firstTypeList != null && firstTypeList.Count > 0)
{
var firstTypeListDistinct = ", " + string.Join(", ", firstTypeList.Where(p => p != "void").Distinct().ToArray());
serviceFileText.Replace("[firstTypeList]",
$"import {{ RestService {firstTypeListDistinct}}} from '@abp/ng.core';");
}
else
{
serviceFileText.Replace("[firstTypeList]",
$"import {{ RestService }} from '@abp/ng.core';");
}
if (secondTypeList != null && secondTypeList.Count > 0)
{
var secondTypeListDistinct = string.Join(", ", secondTypeList.Where(p => p != "void").Distinct().ToArray());
serviceFileText.Replace("[secondTypeList]",
$"import {{{secondTypeListDistinct}}} from '../models';");
}
else
{
serviceFileText.Replace("[secondTypeList]", "");
}
serviceFileText.AppendLine("}");
serviceFileText.Replace("[controllerName]", controllerName);
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services/{controllerServiceName}", serviceFileText.ToString());
var serviceIndexFileText = new StringBuilder();
foreach (var serviceIndexItem in serviceIndexList.Distinct())
{
serviceIndexFileText.AppendLine($"export * from './{serviceIndexItem}';");
}
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/services/index.ts", serviceIndexFileText.ToString());
if (modelIndexList.Count > 0)
{
var modelIndexFileText = new StringBuilder();
foreach (var modelIndexItem in modelIndexList.Distinct())
{
modelIndexFileText.AppendLine($"export * from './{modelIndexItem}';");
}
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/index.ts", modelIndexFileText.ToString());
}
}
}
Logger.LogInformation("Completed!");
}
private Dictionary<string, string> GetCombinedModules(JToken data, string module, out Dictionary<string, string> apiNameList)
{
var moduleList = new Dictionary<string, string>();
apiNameList = new Dictionary<string, string>();
foreach (var moduleItem in data["modules"])
{
var rootPath = ((string)moduleItem.First["rootPath"]).ToLower();
var apiName = (string)moduleItem.First["remoteServiceName"];
if (moduleList.Any(p => p.Key == rootPath))
{
var value = moduleList[rootPath];
moduleList[rootPath] = value.TrimEnd('}') + "," + moduleItem.First["controllers"].ToString().TrimStart('{');
}
else
{
apiNameList.Add(rootPath, apiName);
moduleList.Add(rootPath, moduleItem.First["controllers"].ToString());
}
}
if (module != "all")
{
moduleList = moduleList.Where(p => p.Key.ToLower() == module).ToDictionary(p => p.Key, s => s.Value);
}
return moduleList;
}
private static string CreateType(JObject data, string returnValueType, string rootPath, List<string> modelIndexList, string controllerPathName)
{
var type = data["types"][returnValueType];
if (type == null)
{
return null;
}
if (returnValueType.StartsWith("Volo.Abp.Application.Dtos")
|| returnValueType.StartsWith("System.Collections")
|| returnValueType == "System.String"
|| returnValueType == "System.Void"
|| returnValueType.Contains("System.Net.HttpStatusCode?")
|| returnValueType.Contains("IActionResult")
|| returnValueType.Contains("ActionResult")
|| returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IValueValidator")
)
{
if (returnValueType.Contains("<"))
{
returnValueType = returnValueType.Split('<')[1].Split('>')[0];
var clrType = Type.GetType(returnValueType, throwOnError: false);
if (clrType != null && TypeHelper.IsPrimitiveExtended(clrType, includeEnums: true))
{
return null;
}
if (returnValueType.StartsWith("Volo.Abp.Application.Dtos")
|| returnValueType.StartsWith("System.Collections")
|| returnValueType == "System.Void"
|| returnValueType.Contains("System.Net.HttpStatusCode?")
|| returnValueType.Contains("IActionResult")
|| returnValueType.Contains("ActionResult")
|| returnValueType.Contains("IStringValueType")
|| returnValueType.Contains("IValueValidator")
)
{
return null;
}
}
else
{
return null;
}
}
var typeNameSplit = returnValueType.Split(".");
var typeName = typeNameSplit[typeNameSplit.Length - 1];
var typeModelName = typeName.Replace("<", "").Replace(">", "").Replace("?", "").PascalToKebabCase() + ".ts";
var path = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}";
if (File.Exists(path))
{
return null;
}
var modelFileText = new StringBuilder();
var baseType = (string)type["baseType"];
var extends = "";
var customBaseTypeName = "";
var baseTypeName = "";
if (!string.IsNullOrWhiteSpace(baseType))
{
var baseTypeSplit = baseType.Split(".");
baseTypeName = baseTypeSplit[baseTypeSplit.Length - 1].Replace("<", "").Replace(">", "");
var baseTypeKebabCase = "./" + baseTypeName.PascalToKebabCase();
if (baseType != "System.Enum")
{
if (baseType.Contains("Volo.Abp.Application.Dtos"))
{
baseTypeKebabCase = "@abp/ng.core";
baseTypeName = baseType.Split("Volo.Abp.Application.Dtos")[1].Split("<")[0].TrimStart('.');
customBaseTypeName = baseType.Split("Volo.Abp.Application.Dtos")[1].TrimStart('.');
if (customBaseTypeName.Contains("<"))
{
var genericType = customBaseTypeName.Split("<")[1].TrimEnd('>');
var customBaseType = Type.GetType(genericType);
if (customBaseType != null)
{
customBaseTypeName = customBaseTypeName.Replace(genericType, TypeHelper.GetSimplifiedName(customBaseType));
}
}
}
if (baseTypeName.Contains("guid") || baseTypeName.Contains("Guid"))
{
baseTypeName = "string";
}
modelFileText.AppendLine(Environment.NewLine);
modelFileText.AppendLine($"import {{ {baseTypeName} }} from '{baseTypeKebabCase}';");
extends = "extends " + (!string.IsNullOrWhiteSpace(customBaseTypeName) ? customBaseTypeName : baseTypeName);
var modelIndex = CreateType(data, baseType, rootPath, modelIndexList, controllerPathName);
if (!string.IsNullOrWhiteSpace(modelIndex))
{
modelIndexList.Add(modelIndex);
}
}
}
if (baseType == "System.Enum" && (bool)type["isEnum"])
{
modelFileText.AppendLine($"export enum {typeName} {{");
var enumNameList = type["enumNames"].ToArray();
var enumValueList = type["enumValues"].ToArray();
for (var i = 0; i < enumNameList.Length; i++)
{
modelFileText.AppendLine($" {enumNameList[i]} = {enumValueList[i]},");
}
modelFileText.AppendLine("}");
}
else
{
modelFileText.AppendLine("");
modelFileText.AppendLine($"export class {typeName} {extends} {{");
foreach (var property in type["properties"])
{
var propertyName = (string)property["name"];
propertyName = (char.ToLower(propertyName[0]) + propertyName.Substring(1));
var typeSimple = (string)property["typeSimple"];
var modelIndex = CreateType(data, (string)property["type"], rootPath, modelIndexList, controllerPathName);
if (typeSimple.IndexOf("[") > -1 && typeSimple.IndexOf("]") > -1)
{
typeSimple = typeSimple.Replace("[", "").Replace("]", "") + "[]";
}
if (typeSimple.StartsWith("Volo.Abp"))
{
var typeSimpleSplit = typeSimple.Split(".");
typeSimple = typeSimpleSplit[typeSimpleSplit.Length - 1];
}
if (typeSimple.StartsWith("System.Object"))
{
typeSimple = typeSimple.Replace("System.Object", "object");
}
if (typeSimple.Contains("?"))
{
typeSimple = typeSimple.Replace("?", "");
propertyName += "?";
}
if (typeSimple != "boolean"
&& typeSimple != "string"
&& typeSimple != "number"
&& typeSimple != "boolean[]"
&& typeSimple != "string[]"
&& typeSimple != "number[]"
)
{
var typeSimpleModelName = typeSimple.PascalToKebabCase() + ".ts";
var modelPath = output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeSimpleModelName}";
if (!File.Exists(modelPath))
{
typeSimple = "any" + (typeSimple.Contains("[]") ? "[]" : "");
}
}
if (propertyList.Any(p => p.Key == baseTypeName && p.Value.Any(q => q.Key == propertyName && q.Value == typeSimple)))
{
continue;
}
if (!string.IsNullOrWhiteSpace(modelIndex))
{
var from = "../models";
var propertyTypeSplit = ((string)property["type"]).Split(".");
var propertyType = propertyTypeSplit[propertyTypeSplit.Length - 1];
var propertyTypeKebabCase = propertyType.PascalToKebabCase();
if (File.Exists(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{propertyTypeKebabCase}.ts"))
{
from = "./" + propertyTypeKebabCase;
}
modelFileText.Insert(0, $"import {{ {propertyType} }} from '{from}';");
modelFileText.Insert(0, Environment.NewLine);
modelIndexList.Add(modelIndex);
}
if (propertyList.Any(p => p.Key == typeName && !p.Value.Any(q => q.Key == propertyName)))
{
propertyList[typeName].Add(propertyName, typeSimple);
}
else if (!propertyList.Any(p => p.Key == typeName))
{
var newProperty = new Dictionary<string, string>();
newProperty.Add(propertyName, typeSimple);
propertyList.Add(typeName, newProperty);
}
modelFileText.AppendLine($" {propertyName}: {typeSimple};");
}
modelFileText.AppendLine("");
modelFileText.AppendLine($" constructor(initialValues: Partial<{typeName}> = {{}}) {{");
if (!string.IsNullOrWhiteSpace(baseType))
{
modelFileText.AppendLine(" super(initialValues);");
modelFileText.AppendLine(" }");
}
else
{
modelFileText.AppendLine(" if (initialValues) {");
modelFileText.AppendLine(" for (const key in initialValues) {");
modelFileText.AppendLine(" if (initialValues.hasOwnProperty(key)) {");
modelFileText.AppendLine(" this[key] = initialValues[key];");
modelFileText.AppendLine(" }");
modelFileText.AppendLine(" }");
modelFileText.AppendLine(" }");
modelFileText.AppendLine(" }");
}
modelFileText.AppendLine("}");
}
File.WriteAllText(output + $"{outputPrefix}/{rootPath}/{controllerPathName}/models/{typeModelName}", modelFileText.ToString());
return typeModelName.Replace(".ts", "");
}
private static List<ParameterModel> AddParameter(string parameterName, string type, bool parameterIsOptional, string parameterDefaultValue, string bindingSourceId, List<ParameterModel> parameterModel)
{
if (parameterDefaultValue != "null")
{
parameterModel.Add(new ParameterModel
{
DisplayOrder = 3,
Name = parameterName,
Type = type,
Value = parameterDefaultValue,
BindingSourceId = bindingSourceId
});
}
else if (parameterDefaultValue == "null" && parameterIsOptional)
{
parameterModel.Add(new ParameterModel
{
DisplayOrder = 2,
Name = parameterName,
IsOptional = true,
Type = type,
BindingSourceId = bindingSourceId
});
}
else
{
parameterModel.Add(new ParameterModel
{
DisplayOrder = 1,
Name = parameterName,
Type = type,
BindingSourceId = bindingSourceId
});
}
return parameterModel;
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("Usage:");
sb.AppendLine("");
sb.AppendLine(" abp generate-proxy [options]");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-a|--apiUrl <api-url> (default: environment.ts>apis>default>url)");
sb.AppendLine("-u|--ui <ui-framework> (default: angular)");
sb.AppendLine("-m|--module <module> (default: app)");
sb.AppendLine("");
sb.AppendLine("Examples:");
sb.AppendLine("");
sb.AppendLine(" abp generate-proxy --apiUrl https://www.volosoft.com");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
return sb.ToString();
}
public string GetShortDescription()
{
return "Generates typescript service proxies and DTOs";
}
private UiFramework GetUiFramework(CommandLineArgs commandLineArgs)
{
var optionValue = commandLineArgs.Options.GetOrNull(Options.UiFramework.Short, Options.UiFramework.Long);
switch (optionValue)
{
case "none":
return UiFramework.None;
case "mvc":
return UiFramework.Mvc;
case "angular":
return UiFramework.Angular;
default:
return UiFramework.Angular;
}
}
public static class Options
{
public static class Module
{
public const string Short = "m";
public const string Long = "module";
}
public static class ApiUrl
{
public const string Short = "a";
public const string Long = "apiUrl";
}
public static class UiFramework
{
public const string Short = "u";
public const string Long = "ui";
}
public static class Output
{
public const string Short = "o";
public const string Long = "out";
}
}
}
public static class StringExtensions
public class GenerateProxyCommand : ProxyCommandBase
{
public static string PascalToKebabCase(this string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
public const string Name = "generate-proxy";
using (CultureHelper.Use(CultureInfo.InvariantCulture))
{
return Regex.Replace(
value,
"(?<!^)([A-Z][a-z]|(?<=[a-z])[A-Z])",
"-$1",
RegexOptions.Compiled)
.Trim()
.ToLower();
}
}
}
protected override string CommandName => Name;
public class ParameterModel
{
public int DisplayOrder { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public string Type { get; set; }
public bool IsOptional { get; set; }
public string BindingSourceId { get; set; }
protected override string SchematicsCommandName => "proxy-add";
}
}

144
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs

@ -0,0 +1,144 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands
{
public abstract class ProxyCommandBase : IConsoleCommand, ITransientDependency
{
protected abstract string CommandName { get; }
protected abstract string SchematicsCommandName { get; }
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
CheckAngularJsonFile();
CheckNgSchematics();
var prompt = commandLineArgs.Options.ContainsKey("p") || commandLineArgs.Options.ContainsKey("prompt");
var defaultValue = prompt ? null : "__default";
var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long) ?? defaultValue;
var source = commandLineArgs.Options.GetOrNull(Options.Source.Short, Options.Source.Long) ?? defaultValue;
var target = commandLineArgs.Options.GetOrNull(Options.Target.Short, Options.Target.Long) ?? defaultValue;
var commandBuilder = new StringBuilder("npx ng g @abp/ng.schematics:" + SchematicsCommandName);
if (module != null)
{
commandBuilder.Append($" --module {module}");
}
if (source != null)
{
commandBuilder.Append($" --source {source}");
}
if (target != null)
{
commandBuilder.Append($" --target {target}");
}
CmdHelper.RunCmd(commandBuilder.ToString());
return Task.CompletedTask;
}
private void CheckNgSchematics()
{
var packageJsonPath = $"package.json";
if (!File.Exists(packageJsonPath))
{
throw new CliUsageException(
"package.json file not found" +
Environment.NewLine +
GetUsageInfo()
);
}
var schematicsPackageNode =
(string) JObject.Parse(File.ReadAllText(packageJsonPath))["devDependencies"]?["@abp/ng.schematics"];
if (schematicsPackageNode == null)
{
throw new CliUsageException(
"\"@abp/ng.schematics\" NPM package should be installed to the devDependencies before running this command!" +
Environment.NewLine +
GetUsageInfo()
);
}
}
private void CheckAngularJsonFile()
{
var angularPath = $"angular.json";
if (!File.Exists(angularPath))
{
throw new CliUsageException(
"angular.json file not found. You must run this command in the angular folder." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("Usage:");
sb.AppendLine("");
sb.AppendLine($" abp {CommandName}");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-m|--module <module-name> (default: 'app') The name of the backend module you wish to generate proxies for.");
sb.AppendLine("-s|--source <source-name> (default: 'defaultProject') Angular project name to resolve the root namespace & API definition URL from.");
sb.AppendLine("-t|--target <target-name> (default: 'defaultProject') Angular project name to place generated code in.");
sb.AppendLine("-p|--prompt Asks the options from the command line prompt (for the missing options)");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
return sb.ToString();
}
public string GetShortDescription()
{
return "Generates Angular service proxies and DTOs to consume HTTP APIs.";
}
public static class Options
{
public static class Module
{
public const string Short = "m";
public const string Long = "module";
}
public static class Source
{
public const string Short = "s";
public const string Long = "source";
}
public static class Target
{
public const string Short = "t";
public const string Long = "target";
}
public static class Prompt
{
public const string Short = "p";
public const string Long = "prompt";
}
}
}
}

11
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs

@ -0,0 +1,11 @@
namespace Volo.Abp.Cli.Commands
{
public class RemoveProxyCommand : ProxyCommandBase
{
public const string Name = "remove-proxy";
protected override string CommandName => Name;
protected override string SchematicsCommandName => "proxy-remove";
}
}

87
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs

@ -1,10 +1,13 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.NuGet;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -13,12 +16,14 @@ namespace Volo.Abp.Cli.Commands
public class SuiteCommand : IConsoleCommand, ITransientDependency
{
private readonly AbpNuGetIndexUrlService _nuGetIndexUrlService;
private readonly NuGetService _nuGetService;
private const string SuitePackageName = "Volo.Abp.Suite";
public ILogger<SuiteCommand> Logger { get; set; }
public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService)
public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService, NuGetService nuGetService)
{
_nuGetIndexUrlService = nuGetIndexUrlService;
_nuGetService = nuGetService;
Logger = NullLogger<SuiteCommand>.Instance;
}
@ -26,21 +31,22 @@ namespace Volo.Abp.Cli.Commands
{
var operationType = NamespaceHelper.NormalizeNamespace(commandLineArgs.Target);
var preview = commandLineArgs.Options.ContainsKey(Options.Preview.Long);
switch (operationType)
{
case "":
case null:
await InstallSuiteIfNotInstalledAsync();
RunSuite();
break;
case "install":
Logger.LogInformation("Installing ABP Suite...");
await InstallSuiteAsync();
await InstallSuiteAsync(preview);
break;
case "update":
Logger.LogInformation("Updating ABP Suite...");
await UpdateSuiteAsync();
await UpdateSuiteAsync(preview);
break;
case "remove":
@ -50,8 +56,34 @@ namespace Volo.Abp.Cli.Commands
}
}
private async Task InstallSuiteAsync()
private async Task InstallSuiteIfNotInstalledAsync()
{
var currentSuiteVersionAsString = GetCurrentSuiteVersion();
if (string.IsNullOrEmpty(currentSuiteVersionAsString))
{
await InstallSuiteAsync();
}
}
private string GetCurrentSuiteVersion()
{
var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g");
var suiteLine = dotnetToolList.Split(Environment.NewLine).FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite "));
if (string.IsNullOrEmpty(suiteLine))
{
return null;
}
return suiteLine.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
}
private async Task InstallSuiteAsync(bool preview = false)
{
Logger.LogInformation("Installing ABP Suite...");
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
if (nugetIndexUrl == null)
@ -61,7 +93,9 @@ namespace Volo.Abp.Cli.Commands
try
{
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
var versionOption = await GetVersionOption(preview);
var result = CmdHelper.RunCmd($"dotnet tool install {SuitePackageName} {versionOption} --add-source {nugetIndexUrl} -g");
if (result == 0)
{
@ -86,8 +120,10 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation("dotnet tool install -g Volo.Abp.Suite");
}
private async Task UpdateSuiteAsync()
private async Task UpdateSuiteAsync(bool preview = false)
{
Logger.LogInformation("Updating ABP Suite...");
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
if (nugetIndexUrl == null)
@ -97,7 +133,9 @@ namespace Volo.Abp.Cli.Commands
try
{
var result = CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
var versionOption = await GetVersionOption(preview);
var result = CmdHelper.RunCmd($"dotnet tool update {SuitePackageName} {versionOption} --add-source {nugetIndexUrl} -g");
if (result != 0)
{
@ -111,6 +149,25 @@ namespace Volo.Abp.Cli.Commands
}
}
private async Task<string> GetVersionOption(bool preview)
{
if (preview)
{
var latestVersion = await GetLatestSuiteVersioAsync(true);
if (latestVersion.IsPrerelease)
{
return $"--version {latestVersion.ToString()}";
}
}
return "";
}
private async Task<SemanticVersion> GetLatestSuiteVersioAsync(bool preview)
{
return await _nuGetService.GetLatestVersionOrNullAsync(SuitePackageName, includeReleaseCandidates: preview);
}
private void ShowSuiteManualUpdateCommand()
{
Logger.LogError("You can also run the following command to update ABP Suite.");
@ -160,7 +217,9 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine("");
sb.AppendLine(" abp suite");
sb.AppendLine(" abp suite install");
sb.AppendLine(" abp suite install --preview");
sb.AppendLine(" abp suite update");
sb.AppendLine(" abp suite update --preview");
sb.AppendLine(" abp suite remove");
sb.AppendLine("");
@ -171,5 +230,13 @@ namespace Volo.Abp.Cli.Commands
{
return "Install, update, remove or start ABP Suite. See https://commercial.abp.io/tools/suite.";
}
public static class Options
{
public static class Preview
{
public const string Long = "preview";
}
}
}
}
}

3
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs

@ -86,8 +86,7 @@ namespace Volo.Abp.Cli.NuGet
versions = versions.Where(x => !x.IsPrerelease).ToList();
}
var semanticVersions = versions.ToList();
return semanticVersions.Any() ? semanticVersions.Max() : null;
return versions.Any() ? versions.Max() : null;
}
}

20
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -226,7 +226,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
else
{
if (!switchToStable && SemanticVersion.Parse(currentVersion).IsPrerelease)
if (!switchToStable && IsPrerelease(currentVersion))
{
version = await GetLatestVersion(package, true);
}
@ -236,7 +236,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
if (version == currentVersion)
if (string.IsNullOrEmpty(version) || version == currentVersion)
{
return false;
}
@ -248,6 +248,16 @@ namespace Volo.Abp.Cli.ProjectModification
return true;
}
protected virtual bool IsPrerelease(string version)
{
if (version == null)
{
return false;
}
return version.Split("-", StringSplitOptions.RemoveEmptyEntries).Length > 1;
}
protected virtual async Task<string> GetLatestVersion(
JProperty package,
bool includeReleaseCandidates = false)
@ -265,6 +275,12 @@ namespace Volo.Abp.Cli.ProjectModification
? versionList.First()
: versionList.FirstOrDefault(v => !SemanticVersion.Parse(v).IsPrerelease);
if (string.IsNullOrEmpty(newVersion))
{
_fileVersionStorage[package.Name] = newVersion;
return newVersion;
}
var newVersionWithPrefix = $"~{newVersion}";
_fileVersionStorage[package.Name] = newVersionWithPrefix;

68
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackagePreviewSwitcher.cs

@ -31,6 +31,7 @@ namespace Volo.Abp.Cli.ProjectModification
{
var solutionPath = GetSolutionPath(commandLineArgs);
var solutionFolder = GetSolutionFolder(commandLineArgs);
var solutionAngularFolder = GetSolutionAngularFolder(solutionFolder);
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
solutionPath,
@ -40,42 +41,74 @@ namespace Volo.Abp.Cli.ProjectModification
solutionFolder,
false,
true);
if (solutionAngularFolder != null)
{
await _npmPackagesUpdater.Update(
solutionAngularFolder,
false,
true);
}
}
public async Task SwitchToNightlyPreview(CommandLineArgs commandLineArgs)
{
var solutionPath = GetSolutionPath(commandLineArgs);
var solutionFolder = GetSolutionFolder(commandLineArgs);
var solutionAngularFolder = GetSolutionAngularFolder(solutionFolder);
_packageSourceManager.Add(solutionFolder, "ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json");
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
solutionPath,
true);
if (solutionPath != null)
{
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
solutionPath,
true);
}
await _npmPackagesUpdater.Update(
solutionFolder,
true);
if (solutionAngularFolder != null)
{
await _npmPackagesUpdater.Update(
solutionAngularFolder,
true);
}
}
public async Task SwitchToStable(CommandLineArgs commandLineArgs)
{
var solutionPath = GetSolutionPath(commandLineArgs);
var solutionFolder = GetSolutionFolder(commandLineArgs);
var solutionAngularFolder = GetSolutionAngularFolder(solutionFolder);
_packageSourceManager.Remove(solutionFolder, "ABP Nightly");
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
solutionPath,
false,
false,
true);
if (solutionPath != null)
{
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
solutionPath,
false,
false,
true);
}
await _npmPackagesUpdater.Update(
solutionFolder,
false,
false,
true);
if (solutionAngularFolder != null)
{
await _npmPackagesUpdater.Update(
solutionAngularFolder,
false,
false,
true);
}
}
private string GetSolutionPath(CommandLineArgs commandLineArgs)
@ -99,7 +132,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
Logger.LogError("There is no solution or more that one solution in current directory.");
Logger.LogWarning("There is no solution or more that one solution in current directory.");
return null;
}
@ -112,6 +145,23 @@ namespace Volo.Abp.Cli.ProjectModification
?? Directory.GetCurrentDirectory();
}
private string GetSolutionAngularFolder(string solutionFolder)
{
var upperAngularPath = Path.Combine(Directory.GetParent(solutionFolder)?.FullName ?? "", "angular");
if (Directory.Exists(upperAngularPath))
{
return upperAngularPath;
}
var innerAngularPath = Path.Combine(solutionFolder, "angular");
if (Directory.Exists(innerAngularPath))
{
return innerAngularPath;
}
return null;
}
public static class Options
{
public static class SolutionDirectory

3
framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

16
framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
<ProjectReference Include="..\Volo.Abp.Kafka\Volo.Abp.Kafka.csproj" />
</ItemGroup>
</Project>

27
framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs

@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Kafka;
using Volo.Abp.Modularity;
namespace Volo.Abp.EventBus.Kafka
{
[DependsOn(
typeof(AbpEventBusModule),
typeof(AbpKafkaModule))]
public class AbpEventBusKafkaModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpKafkaEventBusOptions>(configuration.GetSection("Kafka:EventBus"));
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
context
.ServiceProvider
.GetRequiredService<KafkaDistributedEventBus>()
.Initialize();
}
}
}

12
framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs

@ -0,0 +1,12 @@
namespace Volo.Abp.EventBus.Kafka
{
public class AbpKafkaEventBusOptions
{
public string ConnectionName { get; set; }
public string TopicName { get; set; }
public string GroupId { get; set; }
}
}

208
framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs

@ -0,0 +1,208 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Confluent.Kafka;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Kafka;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
namespace Volo.Abp.EventBus.Kafka
{
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IDistributedEventBus), typeof(KafkaDistributedEventBus))]
public class KafkaDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency
{
protected AbpKafkaEventBusOptions AbpKafkaEventBusOptions { get; }
protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; }
protected IKafkaMessageConsumerFactory MessageConsumerFactory { get; }
protected IKafkaSerializer Serializer { get; }
protected IProducerPool ProducerPool { get; }
protected ConcurrentDictionary<Type, List<IEventHandlerFactory>> HandlerFactories { get; }
protected ConcurrentDictionary<string, Type> EventTypes { get; }
protected IKafkaMessageConsumer Consumer { get; private set; }
public KafkaDistributedEventBus(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IOptions<AbpKafkaEventBusOptions> abpKafkaEventBusOptions,
IKafkaMessageConsumerFactory messageConsumerFactory,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
IKafkaSerializer serializer,
IProducerPool producerPool)
: base(serviceScopeFactory, currentTenant)
{
AbpKafkaEventBusOptions = abpKafkaEventBusOptions.Value;
AbpDistributedEventBusOptions = abpDistributedEventBusOptions.Value;
MessageConsumerFactory = messageConsumerFactory;
Serializer = serializer;
ProducerPool = producerPool;
HandlerFactories = new ConcurrentDictionary<Type, List<IEventHandlerFactory>>();
EventTypes = new ConcurrentDictionary<string, Type>();
}
public void Initialize()
{
Consumer = MessageConsumerFactory.Create(
AbpKafkaEventBusOptions.TopicName,
AbpKafkaEventBusOptions.GroupId,
AbpKafkaEventBusOptions.ConnectionName);
Consumer.OnMessageReceived(ProcessEventAsync);
SubscribeHandlers(AbpDistributedEventBusOptions.Handlers);
}
private async Task ProcessEventAsync(Message<string, byte[]> message)
{
var eventName = message.Key;
var eventType = EventTypes.GetOrDefault(eventName);
if (eventType == null)
{
return;
}
var eventData = Serializer.Deserialize(message.Value, eventType);
await TriggerHandlersAsync(eventType, eventData);
}
public IDisposable Subscribe<TEvent>(IDistributedEventHandler<TEvent> handler) where TEvent : class
{
return Subscribe(typeof(TEvent), handler);
}
public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory)
{
var handlerFactories = GetOrCreateHandlerFactories(eventType);
if (factory.IsInFactories(handlerFactories))
{
return NullDisposable.Instance;
}
handlerFactories.Add(factory);
return new EventHandlerFactoryUnregistrar(this, eventType, factory);
}
/// <inheritdoc/>
public override void Unsubscribe<TEvent>(Func<TEvent, Task> action)
{
Check.NotNull(action, nameof(action));
GetOrCreateHandlerFactories(typeof(TEvent))
.Locking(factories =>
{
factories.RemoveAll(
factory =>
{
var singleInstanceFactory = factory as SingleInstanceHandlerFactory;
if (singleInstanceFactory == null)
{
return false;
}
var actionHandler = singleInstanceFactory.HandlerInstance as ActionEventHandler<TEvent>;
if (actionHandler == null)
{
return false;
}
return actionHandler.Action == action;
});
});
}
/// <inheritdoc/>
public override void Unsubscribe(Type eventType, IEventHandler handler)
{
GetOrCreateHandlerFactories(eventType)
.Locking(factories =>
{
factories.RemoveAll(
factory =>
factory is SingleInstanceHandlerFactory handlerFactory &&
handlerFactory.HandlerInstance == handler
);
});
}
/// <inheritdoc/>
public override void Unsubscribe(Type eventType, IEventHandlerFactory factory)
{
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory));
}
/// <inheritdoc/>
public override void UnsubscribeAll(Type eventType)
{
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
public override async Task PublishAsync(Type eventType, object eventData)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);
var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName);
await producer.ProduceAsync(
AbpKafkaEventBusOptions.TopicName,
new Message<string, byte[]>
{
Key = eventName, Value = body
});
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return HandlerFactories.GetOrAdd(
eventType,
type =>
{
var eventName = EventNameAttribute.GetNameOrDefault(type);
EventTypes[eventName] = type;
return new List<IEventHandlerFactory>();
}
);
}
protected override IEnumerable<EventTypeWithEventHandlerFactories> GetHandlerFactories(Type eventType)
{
var handlerFactoryList = new List<EventTypeWithEventHandlerFactories>();
foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))
)
{
handlerFactoryList.Add(
new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value));
}
return handlerFactoryList.ToArray();
}
private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType)
{
//Should trigger same type
if (handlerEventType == targetEventType)
{
return true;
}
//Should trigger for inherited types
if (handlerEventType.IsAssignableFrom(targetEventType))
{
return true;
}
return false;
}
}
}

3
framework/src/Volo.Abp.Kafka/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.Kafka/FodyWeavers.xsd

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

17
framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="1.5.0" />
<ProjectReference Include="..\Volo.Abp.Json\Volo.Abp.Json.csproj" />
<ProjectReference Include="..\Volo.Abp.Threading\Volo.Abp.Threading.csproj" />
</ItemGroup>
</Project>

31
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs

@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
namespace Volo.Abp.Kafka
{
[DependsOn(
typeof(AbpJsonModule),
typeof(AbpThreadingModule)
)]
public class AbpKafkaModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpKafkaOptions>(configuration.GetSection("Kafka"));
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
context.ServiceProvider
.GetRequiredService<IConsumerPool>()
.Dispose();
context.ServiceProvider
.GetRequiredService<IProducerPool>()
.Dispose();
}
}
}

22
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs

@ -0,0 +1,22 @@
using System;
using Confluent.Kafka;
using Confluent.Kafka.Admin;
namespace Volo.Abp.Kafka
{
public class AbpKafkaOptions
{
public KafkaConnections Connections { get; }
public Action<ProducerConfig> ConfigureProducer { get; set; }
public Action<ConsumerConfig> ConfigureConsumer { get; set; }
public Action<TopicSpecification> ConfigureTopic { get; set; }
public AbpKafkaOptions()
{
Connections = new KafkaConnections();
}
}
}

109
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs

@ -0,0 +1,109 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Confluent.Kafka;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Kafka
{
public class ConsumerPool : IConsumerPool, ISingletonDependency
{
protected AbpKafkaOptions Options { get; }
protected ConcurrentDictionary<string, IConsumer<string, byte[]>> Consumers { get; }
protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10);
public ILogger<ConsumerPool> Logger { get; set; }
private bool _isDisposed;
public ConsumerPool(IOptions<AbpKafkaOptions> options)
{
Options = options.Value;
Consumers = new ConcurrentDictionary<string, IConsumer<string, byte[]>>();
Logger = new NullLogger<ConsumerPool>();
}
public virtual IConsumer<string, byte[]> Get(string groupId, string connectionName = null)
{
connectionName ??= KafkaConnections.DefaultConnectionName;
return Consumers.GetOrAdd(
connectionName, connection =>
{
var config = new ConsumerConfig(Options.Connections.GetOrDefault(connection))
{
GroupId = groupId,
EnableAutoCommit = false
};
Options.ConfigureConsumer?.Invoke(config);
return new ConsumerBuilder<string, byte[]>(config).Build();
}
);
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!Consumers.Any())
{
Logger.LogDebug($"Disposed consumer pool with no consumers in the pool.");
return;
}
var poolDisposeStopwatch = Stopwatch.StartNew();
Logger.LogInformation($"Disposing consumer pool ({Consumers.Count} consumers).");
var remainingWaitDuration = TotalDisposeWaitDuration;
foreach (var consumer in Consumers.Values)
{
var poolItemDisposeStopwatch = Stopwatch.StartNew();
try
{
consumer.Close();
consumer.Dispose();
}
catch
{
}
poolItemDisposeStopwatch.Stop();
remainingWaitDuration = remainingWaitDuration > poolItemDisposeStopwatch.Elapsed
? remainingWaitDuration.Subtract(poolItemDisposeStopwatch.Elapsed)
: TimeSpan.Zero;
}
poolDisposeStopwatch.Stop();
Logger.LogInformation(
$"Disposed Kafka Consumer Pool ({Consumers.Count} consumers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms).");
if (poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0)
{
Logger.LogWarning(
$"Disposing Kafka Consumer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms.");
}
Consumers.Clear();
}
}
}

10
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs

@ -0,0 +1,10 @@
using System;
using Confluent.Kafka;
namespace Volo.Abp.Kafka
{
public interface IConsumerPool : IDisposable
{
IConsumer<string, byte[]> Get(string groupId, string connectionName = null);
}
}

11
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
using Confluent.Kafka;
namespace Volo.Abp.Kafka
{
public interface IKafkaMessageConsumer
{
void OnMessageReceived(Func<Message<string, byte[]>, Task> callback);
}
}

19
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs

@ -0,0 +1,19 @@
namespace Volo.Abp.Kafka
{
public interface IKafkaMessageConsumerFactory
{
/// <summary>
/// Creates a new <see cref="IKafkaMessageConsumer"/>.
/// Avoid to create too many consumers since they are
/// not disposed until end of the application.
/// </summary>
/// <param name="topicName"></param>
/// <param name="groupId"></param>
/// <param name="connectionName"></param>
/// <returns></returns>
IKafkaMessageConsumer Create(
string topicName,
string groupId,
string connectionName = null);
}
}

11
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs

@ -0,0 +1,11 @@
using System;
namespace Volo.Abp.Kafka
{
public interface IKafkaSerializer
{
byte[] Serialize(object obj);
object Deserialize(byte[] value, Type type);
}
}

10
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs

@ -0,0 +1,10 @@
using System;
using Confluent.Kafka;
namespace Volo.Abp.Kafka
{
public interface IProducerPool : IDisposable
{
IProducer<string, byte[]> Get(string connectionName = null);
}
}

35
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Confluent.Kafka;
using JetBrains.Annotations;
namespace Volo.Abp.Kafka
{
[Serializable]
public class KafkaConnections : Dictionary<string, ClientConfig>
{
public const string DefaultConnectionName = "Default";
[NotNull]
public ClientConfig Default
{
get => this[DefaultConnectionName];
set => this[DefaultConnectionName] = Check.NotNull(value, nameof(value));
}
public KafkaConnections()
{
Default = new ClientConfig();
}
public ClientConfig GetOrDefault(string connectionName)
{
if (TryGetValue(connectionName, out var connectionFactory))
{
return connectionFactory;
}
return Default;
}
}
}

156
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs

@ -0,0 +1,156 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Confluent.Kafka;
using Confluent.Kafka.Admin;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;
namespace Volo.Abp.Kafka
{
public class KafkaMessageConsumer : IKafkaMessageConsumer, ITransientDependency, IDisposable
{
public ILogger<KafkaMessageConsumer> Logger { get; set; }
protected IConsumerPool ConsumerPool { get; }
protected IExceptionNotifier ExceptionNotifier { get; }
protected AbpKafkaOptions Options { get; }
protected ConcurrentBag<Func<Message<string, byte[]>, Task>> Callbacks { get; }
protected IConsumer<string, byte[]> Consumer { get; private set; }
protected string ConnectionName { get; private set; }
protected string GroupId { get; private set; }
protected string TopicName { get; private set; }
public KafkaMessageConsumer(
IConsumerPool consumerPool,
IExceptionNotifier exceptionNotifier,
IOptions<AbpKafkaOptions> options)
{
ConsumerPool = consumerPool;
ExceptionNotifier = exceptionNotifier;
Options = options.Value;
Logger = NullLogger<KafkaMessageConsumer>.Instance;
Callbacks = new ConcurrentBag<Func<Message<string, byte[]>, Task>>();
}
public virtual void Initialize(
[NotNull] string topicName,
[NotNull] string groupId,
string connectionName = null)
{
Check.NotNull(topicName, nameof(topicName));
Check.NotNull(groupId, nameof(groupId));
TopicName = topicName;
ConnectionName = connectionName ?? KafkaConnections.DefaultConnectionName;
GroupId = groupId;
AsyncHelper.RunSync(CreateTopicAsync);
Consume();
}
public virtual void OnMessageReceived(Func<Message<string, byte[]>, Task> callback)
{
Callbacks.Add(callback);
}
protected virtual async Task CreateTopicAsync()
{
using (var adminClient = new AdminClientBuilder(Options.Connections.GetOrDefault(ConnectionName)).Build())
{
var topic = new TopicSpecification
{
Name = TopicName,
NumPartitions = 1,
ReplicationFactor = 1
};
Options.ConfigureTopic?.Invoke(topic);
try
{
await adminClient.CreateTopicsAsync(new[] {topic});
}
catch (CreateTopicsException e)
{
if (!e.Error.Reason.Contains($"Topic '{TopicName}' already exists"))
{
throw;
}
}
}
}
protected virtual void Consume()
{
Consumer = ConsumerPool.Get(GroupId, ConnectionName);
Task.Factory.StartNew(async () =>
{
Consumer.Subscribe(TopicName);
while (true)
{
try
{
var consumeResult = Consumer.Consume();
if (consumeResult.IsPartitionEOF)
{
continue;
}
await HandleIncomingMessage(consumeResult);
}
catch (ConsumeException ex)
{
Logger.LogException(ex, LogLevel.Warning);
AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning));
}
}
});
}
protected virtual async Task HandleIncomingMessage(ConsumeResult<string, byte[]> consumeResult)
{
try
{
foreach (var callback in Callbacks)
{
await callback(consumeResult.Message);
}
Consumer.Commit(consumeResult);
}
catch (Exception ex)
{
Logger.LogException(ex);
await ExceptionNotifier.NotifyAsync(ex);
}
}
public virtual void Dispose()
{
if (Consumer == null)
{
return;
}
Consumer.Close();
Consumer.Dispose();
}
}
}

32
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs

@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Kafka
{
public class KafkaMessageConsumerFactory : IKafkaMessageConsumerFactory, ISingletonDependency, IDisposable
{
protected IServiceScope ServiceScope { get; }
public KafkaMessageConsumerFactory(IServiceScopeFactory serviceScopeFactory)
{
ServiceScope = serviceScopeFactory.CreateScope();
}
public IKafkaMessageConsumer Create(
string topicName,
string groupId,
string connectionName = null)
{
var consumer = ServiceScope.ServiceProvider.GetRequiredService<KafkaMessageConsumer>();
consumer.Initialize(topicName, groupId, connectionName);
return consumer;
}
public void Dispose()
{
ServiceScope?.Dispose();
}
}
}

102
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs

@ -0,0 +1,102 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using Confluent.Kafka;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Kafka
{
public class ProducerPool : IProducerPool, ISingletonDependency
{
protected AbpKafkaOptions Options { get; }
protected ConcurrentDictionary<string, IProducer<string, byte[]>> Producers { get; }
protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10);
public ILogger<ProducerPool> Logger { get; set; }
private bool _isDisposed;
public ProducerPool(IOptions<AbpKafkaOptions> options)
{
Options = options.Value;
Producers = new ConcurrentDictionary<string, IProducer<string, byte[]>>();
Logger = new NullLogger<ProducerPool>();
}
public virtual IProducer<string, byte[]> Get(string connectionName = null)
{
connectionName ??= KafkaConnections.DefaultConnectionName;
return Producers.GetOrAdd(
connectionName, connection =>
{
var config = Options.Connections.GetOrDefault(connection);
Options.ConfigureProducer?.Invoke(new ProducerConfig(config));
return new ProducerBuilder<string, byte[]>(config).Build();
});
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!Producers.Any())
{
Logger.LogDebug($"Disposed producer pool with no producers in the pool.");
return;
}
var poolDisposeStopwatch = Stopwatch.StartNew();
Logger.LogInformation($"Disposing producer pool ({Producers.Count} producers).");
var remainingWaitDuration = TotalDisposeWaitDuration;
foreach (var producer in Producers.Values)
{
var poolItemDisposeStopwatch = Stopwatch.StartNew();
try
{
producer.Dispose();
}
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(
$"Disposing Kafka Producer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms.");
}
Producers.Clear();
}
}
}

27
framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs

@ -0,0 +1,27 @@
using System;
using System.Text;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.Kafka
{
public class Utf8JsonKafkaSerializer : IKafkaSerializer, ITransientDependency
{
private readonly IJsonSerializer _jsonSerializer;
public Utf8JsonKafkaSerializer(IJsonSerializer jsonSerializer)
{
_jsonSerializer = jsonSerializer;
}
public byte[] Serialize(object obj)
{
return Encoding.UTF8.GetBytes(_jsonSerializer.Serialize(obj));
}
public object Deserialize(byte[] value, Type type)
{
return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value));
}
}
}

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json

@ -17,6 +17,7 @@
"Error": "Error",
"AreYouSure": "Are you sure?",
"Cancel": "Cancel",
"Clear": "Clear",
"Yes": "Yes",
"No": "No",
"Ok": "Ok",

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json

@ -17,6 +17,7 @@
"Error": "Hata",
"AreYouSure": "Emin misiniz?",
"Cancel": "Vazgeç",
"Clear": "Temizle",
"Yes": "Evet",
"No": "Hayır",
"Ok": "Tamam",

2
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json

@ -3,7 +3,7 @@
"name": "asp.net",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.shared": "^3.0.5",
"@abp/aspnetcore.mvc.ui.theme.shared": "^3.1.0-rc.4",
"highlight.js": "^9.13.1"
},
"devDependencies": {}

16
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/core/abp.js

@ -706,18 +706,18 @@ var abp = abp || {};
var toLocal = function (date) {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
};
var toUtc = function (date) {
Date.UTC(
return Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),

16
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/abp/jquery/abp.jquery.js

@ -79,13 +79,15 @@ var abp = abp || {};
}
};
new MutationObserver(mutationObserverCallback).observe(
$('body')[0],
{
subtree: true,
childList: true
}
);
$(function(){
new MutationObserver(mutationObserverCallback).observe(
$('body')[0],
{
subtree: true,
childList: true
}
);
});
// AJAX ///////////////////////////////////////////////////////////////////

222
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock

@ -2,30 +2,30 @@
# yarn lockfile v1
"@abp/aspnetcore.mvc.ui.theme.shared@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76"
integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg==
dependencies:
"@abp/aspnetcore.mvc.ui" "~3.0.5"
"@abp/bootstrap" "~3.0.5"
"@abp/bootstrap-datepicker" "~3.0.5"
"@abp/datatables.net-bs4" "~3.0.5"
"@abp/font-awesome" "~3.0.5"
"@abp/jquery-form" "~3.0.5"
"@abp/jquery-validation-unobtrusive" "~3.0.5"
"@abp/lodash" "~3.0.5"
"@abp/luxon" "~3.0.5"
"@abp/malihu-custom-scrollbar-plugin" "~3.0.5"
"@abp/select2" "~3.0.5"
"@abp/sweetalert" "~3.0.5"
"@abp/timeago" "~3.0.5"
"@abp/toastr" "~3.0.5"
"@abp/aspnetcore.mvc.ui@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0"
integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg==
"@abp/aspnetcore.mvc.ui.theme.shared@^3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.1.0-rc.4.tgz#d5dc9d7d032a434ec49554a3f672dfed637edd61"
integrity sha512-IqZ1B/5SohPJ27g/VxFkvk1ljirTw0gJbpqNLXsnfqZ0OAwBM2yhDB6DE+CAKbi/908flzdNSujFvm0/MaleCA==
dependencies:
"@abp/aspnetcore.mvc.ui" "~3.1.0-rc.4"
"@abp/bootstrap" "~3.1.0-rc.4"
"@abp/bootstrap-datepicker" "~3.1.0-rc.4"
"@abp/datatables.net-bs4" "~3.1.0-rc.4"
"@abp/font-awesome" "~3.1.0-rc.4"
"@abp/jquery-form" "~3.1.0-rc.4"
"@abp/jquery-validation-unobtrusive" "~3.1.0-rc.4"
"@abp/lodash" "~3.1.0-rc.4"
"@abp/luxon" "~3.1.0-rc.4"
"@abp/malihu-custom-scrollbar-plugin" "~3.1.0-rc.4"
"@abp/select2" "~3.1.0-rc.4"
"@abp/sweetalert" "~3.1.0-rc.4"
"@abp/timeago" "~3.1.0-rc.4"
"@abp/toastr" "~3.1.0-rc.4"
"@abp/aspnetcore.mvc.ui@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.1.0-rc.4.tgz#3f3f49d967802ab6cb024615642b84391bd00817"
integrity sha512-m55je2589BKMOC6m7CUqZ84iS+GOt5mzTEfviHHCb5jR4l1djzdzBXepDyXrJNC7I0yyYQ23WCkv5Hq9Z6LOlg==
dependencies:
ansi-colors "^4.1.1"
extend-object "^1.0.0"
@ -34,144 +34,144 @@
path "^0.12.7"
rimraf "^3.0.2"
"@abp/bootstrap-datepicker@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540"
integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA==
"@abp/bootstrap-datepicker@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.1.0-rc.4.tgz#8addc10d8db4e2339d5650ebc04ff40d8405565c"
integrity sha512-ZIKQ/rZP+y3P7OPvZvlp/asIbEzKHVqHVrZwkvX6nZk/0n5o/Siu4Hy/FTBIm+d6Zyx2Y40ux6k5uJEG84TBMg==
dependencies:
bootstrap-datepicker "^1.9.0"
"@abp/bootstrap@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880"
integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg==
"@abp/bootstrap@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.1.0-rc.4.tgz#7c92965c2a35edebb3a529bdd97c0fd6596d281c"
integrity sha512-V8/R9EI0rQkTAtvsQwSHf/cQoJXBvQ/JvqUPNysPM+SQXaSvcyojwDBS7iL/QYsOvoK/gQ9mdLMU9ZCBy7DCZA==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
bootstrap "^4.5.0"
bootstrap-v4-rtl "4.4.1-2"
"@abp/core@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8"
integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA==
"@abp/core@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.1.0-rc.4.tgz#ed4c409255a88693592919314fe1eb27775bee68"
integrity sha512-V4qC3f5WjY76fl26rwT/QXgTrIKivCv3qHurrv04Ni68kxC4lRqHEKaElZh5ZLXWUrwTLs8CHTwQ6oD9jZzO8A==
dependencies:
"@abp/utils" "^3.0.5"
"@abp/utils" "^3.1.0-rc.4"
"@abp/datatables.net-bs4@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a"
integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ==
"@abp/datatables.net-bs4@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.1.0-rc.4.tgz#dd42fe0cc9e71a34d72b87b569c8d9bb9c576815"
integrity sha512-TgOIWIA7qgW+Z8zp1RtPoZoE6CNUWYOVC7bJ8ZfwXuBpTbIOJyuzYFLySR2seYae4uePbVZpSmuMNgrtPKkGdA==
dependencies:
"@abp/datatables.net" "~3.0.5"
"@abp/datatables.net" "~3.1.0-rc.4"
datatables.net-bs4 "^1.10.21"
"@abp/datatables.net@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491"
integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew==
"@abp/datatables.net@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.1.0-rc.4.tgz#98861ee58eb2527f07cf3352af55b782cc13e3fc"
integrity sha512-rsQpMhy5QVSzNG0zdhxR0I9lJf10tK0xLOkM+aIjxNjGnylIUeconDABp+bGZeZNOVHo7nWE9g0hL9MDKXhbgw==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
datatables.net "^1.10.21"
"@abp/font-awesome@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412"
integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ==
"@abp/font-awesome@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.1.0-rc.4.tgz#d63ff941b541a29a5689938feb4f681eae33ee99"
integrity sha512-NrU/4E3si71gC6ty9JH5KRqMKco0ZINReOqAqTQelWGshxClybLHjodf6QXuk2GOYK3RAmnUX7/33cm8g0S3zA==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
"@fortawesome/fontawesome-free" "^5.13.0"
"@abp/jquery-form@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78"
integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg==
"@abp/jquery-form@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.1.0-rc.4.tgz#8eb717e1bfa589b45762c418ca7e4a230660d9d4"
integrity sha512-Luz1b2E8rQA34c7UP6tBXKvX40SbwGJ3Q/NoAJEuEBxuWMx/SFZCbQ0JX8OqtyvH7A/GTBM+LqQGC2vXATrm7g==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
jquery-form "^4.3.0"
"@abp/jquery-validation-unobtrusive@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f"
integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw==
"@abp/jquery-validation-unobtrusive@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.1.0-rc.4.tgz#f675c916c7992e1868dd763bd737581d0182d35a"
integrity sha512-rCgtfpDKUOM+nsZ5NC3tLsTZ5TfQch6XJXrrJq5PDwpdyQkbth5vBbEQrWawSy/m0omGJWS074GcqQBdOPCVLw==
dependencies:
"@abp/jquery-validation" "~3.0.5"
"@abp/jquery-validation" "~3.1.0-rc.4"
jquery-validation-unobtrusive "^3.2.11"
"@abp/jquery-validation@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279"
integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng==
"@abp/jquery-validation@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.1.0-rc.4.tgz#d1624a2b232729ac3d71dc4c02d91cdc92044250"
integrity sha512-uID9mOupRGvvevsnHAEZeijLTREY2sVR+eHs9ecxr4c2j2UuFuIk0ebuxMD4yY6PkjH3a/odKlAw3ddtAYVF3Q==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
jquery-validation "^1.19.2"
"@abp/jquery@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754"
integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ==
"@abp/jquery@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.1.0-rc.4.tgz#a143bec2bebd2c22ae1f6fb9d29423627ecbb452"
integrity sha512-jfWIG8vZ8La3pBMxD4B7ffhL1KXdRPZfC/E5jOQmozDxygGC9/2OL/OAPrD0o9cwirYq3Yofd2KbLr0WDIQGRQ==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
jquery "~3.5.1"
"@abp/lodash@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4"
integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA==
"@abp/lodash@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.1.0-rc.4.tgz#2263a2554e44b577d0a25a3eaec9d2d658b9f3f0"
integrity sha512-tXekVHdI55J52J4GUYC5gNz8J9uc85YCnDJA3X5C+eLNJoHd/RJXVemTuQLS0KugZjic0alVbRuTvjt61CkL7A==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
lodash "^4.17.15"
"@abp/luxon@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688"
integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw==
"@abp/luxon@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.1.0-rc.4.tgz#73c667c8fa3e951031b8a3cc217640d704c437f3"
integrity sha512-IlzB1eLBH336gQSxJ/ZKjqXUCGCqS4nssvWNgeK/heONwDdIhqQ6ERYtEoAA0MWxYSJhWcOEkTiiWErnt4YneA==
dependencies:
luxon "^1.24.1"
"@abp/malihu-custom-scrollbar-plugin@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39"
integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q==
"@abp/malihu-custom-scrollbar-plugin@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.1.0-rc.4.tgz#f8ee4c03220e1cd8e3f069a859ba20b6cfe40fcf"
integrity sha512-oq6Od+FukngrvofLrMX1Y94ivRrqNfmu/UDzBzBs95zGW97EPSrYl4/8YUFT2Vp7a7OBf/VCbb9gjGwrLh4u5w==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
malihu-custom-scrollbar-plugin "^3.1.5"
"@abp/select2@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350"
integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw==
"@abp/select2@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.1.0-rc.4.tgz#c97c968544197754b4ed57308672b1f106260019"
integrity sha512-sqKffeoDvItb2CRrQjHGEuLJTY1Uiw9U1xg7VQqd/rRRO6IimvPNsjy2m26uzPrhg9MFdzGW22pMf8oaE8KoYw==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
select2 "^4.0.13"
"@abp/sweetalert@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab"
integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg==
"@abp/sweetalert@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.1.0-rc.4.tgz#7ab26bf10425de59be2c9aabbec44ae5389b0532"
integrity sha512-lZNo2Qo9TUsYkiT64CB3An8EHzzIcqX1FSf706RRJs6sf/hDNhiX0uWeBY8p0OyHTa/cphT/AUsLMkZOXZqmJw==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
sweetalert "^2.1.2"
"@abp/timeago@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31"
integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw==
"@abp/timeago@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.1.0-rc.4.tgz#d4c814e9d90d020eb96325ec2269c9aa7b4d732c"
integrity sha512-EnbmgteH2a98pAPhPWAMbNvE+ilkf7Ro+p6lw7TKZydpuUE/hYHqYbWB8Hd7aVuQQ+Hlki0bnZwV32ID13ZPDA==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
timeago "^1.6.7"
"@abp/toastr@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc"
integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw==
"@abp/toastr@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.1.0-rc.4.tgz#eb73f4b4d938362c66b58edfd16c238b8343cd3a"
integrity sha512-MgxqZdoim9EhUxMztZa7d1aGUsuspLp0FwrSidrPz7Bp1d1sPqTmDwUbHOLDLvvT9GmLPexhI/A3ACX95elPZA==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
toastr "^2.1.4"
"@abp/utils@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49"
integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg==
"@abp/utils@^3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.1.0-rc.4.tgz#e0d25d60b1806d4bee23e1c9c0ffa7855cdffc2a"
integrity sha512-qzUqqkLJcrQV3OngDyQ0S8hEh3D1MxvvlFLOo77ajnaTO2X8hgnFm9mgQ2QnvH2QbKttaWpaaTX7TCYu/d7nBg==
dependencies:
just-compare "^1.3.0"

2
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json

@ -3,7 +3,7 @@
"name": "asp.net",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5"
"@abp/aspnetcore.mvc.ui.theme.basic": "^3.1.0-rc.4"
},
"devDependencies": {}
}

16
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/core/abp.js

@ -706,18 +706,18 @@ var abp = abp || {};
var toLocal = function (date) {
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
};
var toUtc = function (date) {
Date.UTC(
return Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),

16
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/jquery/abp.jquery.js

@ -79,13 +79,15 @@ var abp = abp || {};
}
};
new MutationObserver(mutationObserverCallback).observe(
$('body')[0],
{
subtree: true,
childList: true
}
);
$(function(){
new MutationObserver(mutationObserverCallback).observe(
$('body')[0],
{
subtree: true,
childList: true
}
);
});
// AJAX ///////////////////////////////////////////////////////////////////

236
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock

@ -2,37 +2,37 @@
# yarn lockfile v1
"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.5.tgz#c146f20226522f0305cc0fa31cfcf9d39be78901"
integrity sha512-ptNhD4xV2wSLW7vQ5KUgaRiH5Ov1OscZW2ZEoivnt3kqgtIC3DGw+SiE9vjFCvyY83fjRkIlW2PIfQxSySbteQ==
dependencies:
"@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5"
"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76"
integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg==
dependencies:
"@abp/aspnetcore.mvc.ui" "~3.0.5"
"@abp/bootstrap" "~3.0.5"
"@abp/bootstrap-datepicker" "~3.0.5"
"@abp/datatables.net-bs4" "~3.0.5"
"@abp/font-awesome" "~3.0.5"
"@abp/jquery-form" "~3.0.5"
"@abp/jquery-validation-unobtrusive" "~3.0.5"
"@abp/lodash" "~3.0.5"
"@abp/luxon" "~3.0.5"
"@abp/malihu-custom-scrollbar-plugin" "~3.0.5"
"@abp/select2" "~3.0.5"
"@abp/sweetalert" "~3.0.5"
"@abp/timeago" "~3.0.5"
"@abp/toastr" "~3.0.5"
"@abp/aspnetcore.mvc.ui@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0"
integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg==
"@abp/aspnetcore.mvc.ui.theme.basic@^3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.1.0-rc.4.tgz#54f6e75e2736c9c97a55146e9dc2af82f106c676"
integrity sha512-g5mRdK1F/BbhJVff9W5BIeH0Ca9GClyVXRHOIj9VWxYmR4DI3cQUpKS5uTKBp4injTswLYFntZP4RsEeyBRg2g==
dependencies:
"@abp/aspnetcore.mvc.ui.theme.shared" "~3.1.0-rc.4"
"@abp/aspnetcore.mvc.ui.theme.shared@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.1.0-rc.4.tgz#d5dc9d7d032a434ec49554a3f672dfed637edd61"
integrity sha512-IqZ1B/5SohPJ27g/VxFkvk1ljirTw0gJbpqNLXsnfqZ0OAwBM2yhDB6DE+CAKbi/908flzdNSujFvm0/MaleCA==
dependencies:
"@abp/aspnetcore.mvc.ui" "~3.1.0-rc.4"
"@abp/bootstrap" "~3.1.0-rc.4"
"@abp/bootstrap-datepicker" "~3.1.0-rc.4"
"@abp/datatables.net-bs4" "~3.1.0-rc.4"
"@abp/font-awesome" "~3.1.0-rc.4"
"@abp/jquery-form" "~3.1.0-rc.4"
"@abp/jquery-validation-unobtrusive" "~3.1.0-rc.4"
"@abp/lodash" "~3.1.0-rc.4"
"@abp/luxon" "~3.1.0-rc.4"
"@abp/malihu-custom-scrollbar-plugin" "~3.1.0-rc.4"
"@abp/select2" "~3.1.0-rc.4"
"@abp/sweetalert" "~3.1.0-rc.4"
"@abp/timeago" "~3.1.0-rc.4"
"@abp/toastr" "~3.1.0-rc.4"
"@abp/aspnetcore.mvc.ui@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.1.0-rc.4.tgz#3f3f49d967802ab6cb024615642b84391bd00817"
integrity sha512-m55je2589BKMOC6m7CUqZ84iS+GOt5mzTEfviHHCb5jR4l1djzdzBXepDyXrJNC7I0yyYQ23WCkv5Hq9Z6LOlg==
dependencies:
ansi-colors "^4.1.1"
extend-object "^1.0.0"
@ -41,144 +41,144 @@
path "^0.12.7"
rimraf "^3.0.2"
"@abp/bootstrap-datepicker@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540"
integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA==
"@abp/bootstrap-datepicker@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.1.0-rc.4.tgz#8addc10d8db4e2339d5650ebc04ff40d8405565c"
integrity sha512-ZIKQ/rZP+y3P7OPvZvlp/asIbEzKHVqHVrZwkvX6nZk/0n5o/Siu4Hy/FTBIm+d6Zyx2Y40ux6k5uJEG84TBMg==
dependencies:
bootstrap-datepicker "^1.9.0"
"@abp/bootstrap@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880"
integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg==
"@abp/bootstrap@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.1.0-rc.4.tgz#7c92965c2a35edebb3a529bdd97c0fd6596d281c"
integrity sha512-V8/R9EI0rQkTAtvsQwSHf/cQoJXBvQ/JvqUPNysPM+SQXaSvcyojwDBS7iL/QYsOvoK/gQ9mdLMU9ZCBy7DCZA==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
bootstrap "^4.5.0"
bootstrap-v4-rtl "4.4.1-2"
"@abp/core@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8"
integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA==
"@abp/core@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.1.0-rc.4.tgz#ed4c409255a88693592919314fe1eb27775bee68"
integrity sha512-V4qC3f5WjY76fl26rwT/QXgTrIKivCv3qHurrv04Ni68kxC4lRqHEKaElZh5ZLXWUrwTLs8CHTwQ6oD9jZzO8A==
dependencies:
"@abp/utils" "^3.0.5"
"@abp/utils" "^3.1.0-rc.4"
"@abp/datatables.net-bs4@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a"
integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ==
"@abp/datatables.net-bs4@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.1.0-rc.4.tgz#dd42fe0cc9e71a34d72b87b569c8d9bb9c576815"
integrity sha512-TgOIWIA7qgW+Z8zp1RtPoZoE6CNUWYOVC7bJ8ZfwXuBpTbIOJyuzYFLySR2seYae4uePbVZpSmuMNgrtPKkGdA==
dependencies:
"@abp/datatables.net" "~3.0.5"
"@abp/datatables.net" "~3.1.0-rc.4"
datatables.net-bs4 "^1.10.21"
"@abp/datatables.net@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491"
integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew==
"@abp/datatables.net@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.1.0-rc.4.tgz#98861ee58eb2527f07cf3352af55b782cc13e3fc"
integrity sha512-rsQpMhy5QVSzNG0zdhxR0I9lJf10tK0xLOkM+aIjxNjGnylIUeconDABp+bGZeZNOVHo7nWE9g0hL9MDKXhbgw==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
datatables.net "^1.10.21"
"@abp/font-awesome@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412"
integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ==
"@abp/font-awesome@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.1.0-rc.4.tgz#d63ff941b541a29a5689938feb4f681eae33ee99"
integrity sha512-NrU/4E3si71gC6ty9JH5KRqMKco0ZINReOqAqTQelWGshxClybLHjodf6QXuk2GOYK3RAmnUX7/33cm8g0S3zA==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
"@fortawesome/fontawesome-free" "^5.13.0"
"@abp/jquery-form@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78"
integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg==
"@abp/jquery-form@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.1.0-rc.4.tgz#8eb717e1bfa589b45762c418ca7e4a230660d9d4"
integrity sha512-Luz1b2E8rQA34c7UP6tBXKvX40SbwGJ3Q/NoAJEuEBxuWMx/SFZCbQ0JX8OqtyvH7A/GTBM+LqQGC2vXATrm7g==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
jquery-form "^4.3.0"
"@abp/jquery-validation-unobtrusive@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f"
integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw==
"@abp/jquery-validation-unobtrusive@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.1.0-rc.4.tgz#f675c916c7992e1868dd763bd737581d0182d35a"
integrity sha512-rCgtfpDKUOM+nsZ5NC3tLsTZ5TfQch6XJXrrJq5PDwpdyQkbth5vBbEQrWawSy/m0omGJWS074GcqQBdOPCVLw==
dependencies:
"@abp/jquery-validation" "~3.0.5"
"@abp/jquery-validation" "~3.1.0-rc.4"
jquery-validation-unobtrusive "^3.2.11"
"@abp/jquery-validation@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279"
integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng==
"@abp/jquery-validation@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.1.0-rc.4.tgz#d1624a2b232729ac3d71dc4c02d91cdc92044250"
integrity sha512-uID9mOupRGvvevsnHAEZeijLTREY2sVR+eHs9ecxr4c2j2UuFuIk0ebuxMD4yY6PkjH3a/odKlAw3ddtAYVF3Q==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
jquery-validation "^1.19.2"
"@abp/jquery@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754"
integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ==
"@abp/jquery@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.1.0-rc.4.tgz#a143bec2bebd2c22ae1f6fb9d29423627ecbb452"
integrity sha512-jfWIG8vZ8La3pBMxD4B7ffhL1KXdRPZfC/E5jOQmozDxygGC9/2OL/OAPrD0o9cwirYq3Yofd2KbLr0WDIQGRQ==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
jquery "~3.5.1"
"@abp/lodash@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4"
integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA==
"@abp/lodash@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.1.0-rc.4.tgz#2263a2554e44b577d0a25a3eaec9d2d658b9f3f0"
integrity sha512-tXekVHdI55J52J4GUYC5gNz8J9uc85YCnDJA3X5C+eLNJoHd/RJXVemTuQLS0KugZjic0alVbRuTvjt61CkL7A==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
lodash "^4.17.15"
"@abp/luxon@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688"
integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw==
"@abp/luxon@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.1.0-rc.4.tgz#73c667c8fa3e951031b8a3cc217640d704c437f3"
integrity sha512-IlzB1eLBH336gQSxJ/ZKjqXUCGCqS4nssvWNgeK/heONwDdIhqQ6ERYtEoAA0MWxYSJhWcOEkTiiWErnt4YneA==
dependencies:
luxon "^1.24.1"
"@abp/malihu-custom-scrollbar-plugin@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39"
integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q==
"@abp/malihu-custom-scrollbar-plugin@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.1.0-rc.4.tgz#f8ee4c03220e1cd8e3f069a859ba20b6cfe40fcf"
integrity sha512-oq6Od+FukngrvofLrMX1Y94ivRrqNfmu/UDzBzBs95zGW97EPSrYl4/8YUFT2Vp7a7OBf/VCbb9gjGwrLh4u5w==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
malihu-custom-scrollbar-plugin "^3.1.5"
"@abp/select2@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350"
integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw==
"@abp/select2@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.1.0-rc.4.tgz#c97c968544197754b4ed57308672b1f106260019"
integrity sha512-sqKffeoDvItb2CRrQjHGEuLJTY1Uiw9U1xg7VQqd/rRRO6IimvPNsjy2m26uzPrhg9MFdzGW22pMf8oaE8KoYw==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
select2 "^4.0.13"
"@abp/sweetalert@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab"
integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg==
"@abp/sweetalert@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.1.0-rc.4.tgz#7ab26bf10425de59be2c9aabbec44ae5389b0532"
integrity sha512-lZNo2Qo9TUsYkiT64CB3An8EHzzIcqX1FSf706RRJs6sf/hDNhiX0uWeBY8p0OyHTa/cphT/AUsLMkZOXZqmJw==
dependencies:
"@abp/core" "~3.0.5"
"@abp/core" "~3.1.0-rc.4"
sweetalert "^2.1.2"
"@abp/timeago@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31"
integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw==
"@abp/timeago@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.1.0-rc.4.tgz#d4c814e9d90d020eb96325ec2269c9aa7b4d732c"
integrity sha512-EnbmgteH2a98pAPhPWAMbNvE+ilkf7Ro+p6lw7TKZydpuUE/hYHqYbWB8Hd7aVuQQ+Hlki0bnZwV32ID13ZPDA==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
timeago "^1.6.7"
"@abp/toastr@~3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc"
integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw==
"@abp/toastr@~3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.1.0-rc.4.tgz#eb73f4b4d938362c66b58edfd16c238b8343cd3a"
integrity sha512-MgxqZdoim9EhUxMztZa7d1aGUsuspLp0FwrSidrPz7Bp1d1sPqTmDwUbHOLDLvvT9GmLPexhI/A3ACX95elPZA==
dependencies:
"@abp/jquery" "~3.0.5"
"@abp/jquery" "~3.1.0-rc.4"
toastr "^2.1.4"
"@abp/utils@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49"
integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg==
"@abp/utils@^3.1.0-rc.4":
version "3.1.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.1.0-rc.4.tgz#e0d25d60b1806d4bee23e1c9c0ffa7855cdffc2a"
integrity sha512-qzUqqkLJcrQV3OngDyQ0S8hEh3D1MxvvlFLOo77ajnaTO2X8hgnFm9mgQ2QnvH2QbKttaWpaaTX7TCYu/d7nBg==
dependencies:
just-compare "^1.3.0"

4
modules/blogging/app/Volo.BloggingTestApp/package.json

@ -3,7 +3,7 @@
"name": "volo.blogtestapp",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5",
"@abp/blogging": "^3.0.5"
"@abp/aspnetcore.mvc.ui.theme.basic": "^3.1.0-rc.4",
"@abp/blogging": "^3.1.0-rc.4"
}
}

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

Loading…
Cancel
Save