Browse Source

Merge branch 'dev' into new-hosting-model

pull/10928/head
maliming 4 years ago
parent
commit
04de7b2a63
No known key found for this signature in database GPG Key ID: 96224957E51C89E
  1. 4
      docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md
  2. 2
      docs/en/Tutorials/Part-10.md
  3. 22
      docs/en/Tutorials/Part-2.md
  4. 2
      docs/en/Tutorials/Part-9.md
  5. 2
      docs/en/UI/Angular/Service-Proxies.md
  6. 123
      docs/zh-Hans/Tutorials/Part-1.md
  7. 256
      docs/zh-Hans/Tutorials/Part-2.md
  8. 600
      docs/zh-Hans/Tutorials/Part-3.md
  9. 2
      docs/zh-Hans/Tutorials/Todo/Index.md
  10. BIN
      docs/zh-Hans/Tutorials/images/blazor-add-book-button.png
  11. BIN
      docs/zh-Hans/Tutorials/images/blazor-add-books-component.png
  12. BIN
      docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png
  13. BIN
      docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png
  14. BIN
      docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png
  15. BIN
      docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png
  16. BIN
      docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png
  17. BIN
      docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png
  18. BIN
      docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png
  19. BIN
      docs/zh-Hans/Tutorials/images/generated-proxies-3.png
  20. BIN
      docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png
  21. 2
      docs/zh-Hans/UI/Angular/Service-Proxies.md
  22. BIN
      docs/zh-Hans/images/create-aspnet-core-application.png
  23. 11
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
  24. 54
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs
  25. 12
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinderProvider.cs
  26. 16
      framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/AbpAspNetCoreServiceCollectionExtensions.cs
  27. 19
      framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/EmptyHostingEnvironment.cs
  28. 41
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  29. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs
  30. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
  31. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
  32. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs
  33. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CreateMigrationAndRunMigratorCommand.cs
  34. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs
  35. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
  36. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/InstallLibsCommand.cs
  37. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs
  38. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs
  39. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs
  40. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs
  41. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  42. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/PromptCommand.cs
  43. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/ConnectionStringProvider.cs
  44. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  45. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs
  46. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToPreviewCommand.cs
  47. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToStableCommand.cs
  48. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs
  49. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
  50. 4
      framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
  51. 6
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  52. 66
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs
  53. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateFileInput.cs
  54. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateMultipleFileInput.cs
  55. 29
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json
  56. 2
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs
  57. 7
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpUserClaimsFactory.cs
  58. 2
      nupkg/common.ps1
  59. 2
      test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj
  60. 10
      test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs

4
docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md

@ -12,7 +12,7 @@ The most common question is;
The answer is, **you don't**. ABP Angular application is integrated with the backend (HttpApi.Host project) where it loads the configurations, **permissions** etc. For none-tiered angular applications, **HttpApi.Host** project also has IdentityServer4 embedded; also serving as **Authorization Server**. Angular application authentication flow is shown below.
<img src="auth-diagram.jpeg" alt="auth-diagram" style="zoom:50%;" />
![auth-diagram](auth-diagram.jpeg)
> What if I want Azure AD as my authorization server and not IdentityServer?
@ -80,7 +80,7 @@ Navigate to Manage Azure Active Directory in [azure portal](https://portal.azure
Enter a name for your application and **App.SelfUrl** + **AzureAd.CallbackPath** as redirect uri then register.
<img src="azure-app-register.JPG" alt="azure-app-register" style="zoom:75%;" />
![azure-app-register](azure-app-register.JPG)
Now navigate to **Authentication** on the left menu and enable **ID tokens**.

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

@ -948,7 +948,7 @@ Since the HTTP APIs have been changed, you need to update Angular client side [s
Run the following command in the `angular` folder (you may need to stop the angular application):
```bash
abp generate-proxy
abp generate-proxy -t ng
```
This command will update the service proxy files under the `/src/app/proxy/` folder.

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

@ -10,7 +10,7 @@
In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies:
* **{{DB_Value}}** as the ORM provider.
* **{{DB_Value}}** as the ORM provider.
* **{{UI_Value}}** as the UI Framework.
This tutorial is organized as the following parts;
@ -80,13 +80,13 @@ You can see the **book list** returned from the server. You can also check the *
Let's **create a new book** using the `create` function:
````js
acme.bookStore.books.book.create({
name: 'Foundation',
type: 7,
publishDate: '1951-05-24',
price: 21.5
}).then(function (result) {
console.log('successfully created the book with id: ' + result.id);
acme.bookStore.books.book.create({
name: 'Foundation',
type: 7,
publishDate: '1951-05-24',
price: 21.5
}).then(function (result) {
console.log('successfully created the book with id: ' + result.id);
});
````
@ -187,7 +187,7 @@ namespace Acme.BookStore.Web.Pages.Books
{
public void OnGet()
{
}
}
}
@ -461,7 +461,7 @@ For more information, see the [RoutesService document](../UI/Angular/Modifying-t
Once the host application is running, execute the following command in the `angular` folder:
```bash
abp generate-proxy
abp generate-proxy -t ng
```
This command will create the following files under the `/src/app/proxy/books` folder:
@ -654,7 +654,7 @@ Open the `Books.razor` and replace the content as the following:
> If you see some syntax errors, you can ignore them if your application properly built and run. Visual Studio still has some bugs with Blazor.
* Inherited from the `AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>` which implements all the CRUD details for us.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base blass.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class.
* Injected `IStringLocalizer<BookStoreResource>` (as `L` object) and used for localization.
While the code above pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better.

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

@ -605,7 +605,7 @@ function configureRoutes(routes: RoutesService) {
Run the following command in the `angular` folder:
```bash
abp generate-proxy
abp generate-proxy -t ng
```
This command generates the service proxy for the author service and the related model (DTO) classes:

2
docs/en/UI/Angular/Service-Proxies.md

@ -15,7 +15,7 @@ ABP introduces an endpoint that exposes server-side method contracts. When the `
Run the following command in the **root folder** of the angular application:
```bash
abp generate-proxy
abp generate-proxy -t ng
```
The command without any parameters creates proxies only for your own application's services and places them in your default Angular application. There are several parameters you may use to modify this behavior. See the [CLI documentation](../../CLI) for details.

123
docs/zh-Hans/Tutorials/Part-1.md

@ -2,32 +2,15 @@
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
* **{{DB_Text}}** 做为ORM提供程序.
* **{{DB_Text}}** 做为数据库提供程序.
* **{{UI_Value}}** 做为UI框架.
本教程分为以下部分:
@ -40,16 +23,29 @@ end
- [Part 6: 作者: 领域层](Part-6.md)
- [Part 7: 作者: 数据库集成](Part-7.md)
- [Part 8: 作者: 应用服务层](Part-8.md)
- [Part 9: 作者: 用户](Part-9.md)
- [Part 9: 作者: 用户](Part-9.md)
- [Part 10: 图书到作者的关系](Part-10.md)
## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合:
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=cJzyIFfAlp8&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=1" target="_blank">发布在YouTube</a>**.
{{end}}
## 创建解决方案
在开始开发之前,请按照[入门教程](../Getting-Started.md)创建名为 `Acme.BookStore` 的新解决方案.
@ -58,12 +54,12 @@ end
启动模板中的**领域层**分为两个项目:
- `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象.
- `Acme.BookStore.Domain`包含你的[实体](../Entities.md), [领域服务](../Domain-Services.md)和其他核心域对象.
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象.
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体.
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体.
该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹并在其中添加一个名为 `Book` 的类,如下所示:
该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹(命名空间),并在其中添加名为 `Book` 的类,如下所示:
````csharp
using System;
@ -84,15 +80,15 @@ namespace Acme.BookStore.Books
}
````
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](./Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](../Entities.md)).
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性.
* `Guid`是`Book`实体的主键类型.
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](../Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](../Entities.md)).
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些基础[审计](../Audit-Logging.md)属性(例如`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性.
* `Guid`是`Book`实体的**主键类型**.
> 为了保持简单,本教程将实体属性保留为 **public get/set** . 如果你想了解关于DDD最佳实践,请参阅[实体文档](../Entities.md).
### BookType枚举
上面所用到了 `BookType` 枚举,在 `Acme.BookStore.Domain.Shared` 项目创建 `BookType`.
`Book`实体使用了`BookType`枚举. 在`Acme.BookStore.Domain.Shared`项目中创建`Books`文件夹(命名空间),并在其中添加`BookType`:
````csharp
namespace Acme.BookStore.Books
@ -150,28 +146,37 @@ public class BookStoreMongoDbContext : AbpMongoDbContext
### 将Book实体映射到数据库表
`Acme.BookStore.EntityFrameworkCore` 项目中打开 `BookStoreDbContextModelCreatingExtensions.cs` 文件,添加 `Book` 实体的映射代码. 最终类应为:
打开`BookStoreDbContext`类的`OnModelCreating`方法,为`Book`实体添加映射代码:
````csharp
using Acme.BookStore.Books;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore.Modeling;
...
namespace Acme.BookStore.EntityFrameworkCore
{
public static class BookStoreDbContextModelCreatingExtensions
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
public static void ConfigureBookStore(this ModelBuilder builder)
...
protected override void OnModelCreating(ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
@ -180,14 +185,14 @@ namespace Acme.BookStore.EntityFrameworkCore
}
````
* `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 你不必使用它,但建议在单点控制表前缀.
* `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应始终所有的实体使用它.
* `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 使用它不是强制的,但建议在统一的地方控制表前缀.
* `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应对所有的实体使用它.
### 添加数据迁移
启动模板使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/)创建和维护数据库架构. 我们应该创建一个新的迁移并且应用到数据库.
本示例使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/).因为我们修改了数据库映射配置,我们必须创建一个新的迁移并且应用到数据库.
`Acme.BookStore.EntityFrameworkCore.DbMigrations` 目录打开命令行终端输入以下命令:
`Acme.BookStore.EntityFrameworkCore` 目录打开命令行终端输入以下命令:
```bash
dotnet ef migrations add Created_Book_Entity
@ -205,7 +210,7 @@ dotnet ef migrations add Created_Book_Entity
> >在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的[数据种子系统](../Data-Seeding.md). 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。
`*.Domain` 项目下创建派生 `IDataSeedContributor` 的类,并且拷贝以下代码:
`*.Domain` 项目下创建 `IDataSeedContributor`派生类,并且拷贝以下代码:
```csharp
using System;
@ -258,7 +263,7 @@ namespace Acme.BookStore
}
```
* 如果数据库中当前没有图书,则此代码使用 `IRepository<Book, Guid>`(默认[repository](../Repositories.md))将两本书插入数据库.
* 如果数据库中当前没有图书,则此代码使用 `IRepository<Book, Guid>`(默认[repository](../Repositories.md))将两本书插入数据库.
### 更新数据库
@ -279,7 +284,7 @@ namespace Acme.BookStore
### BookDto
`CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建一个名为 `BookDto` 的DTO类:
`CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建 `Books` 文件夹(命名空间), 并在其中添加名为 `BookDto` 的DTO类:
````C#
using System;
@ -300,9 +305,9 @@ namespace Acme.BookStore
}
````
* **DTO**类被用来在 **表示层****应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息.
* 为了在面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层.
* `BookDto`继承自 `AuditedEntityDto<Guid>`.上面定义的 `Book` 实体一样具有一些审计属性.
* **DTO**类被用来在 **表示层****应用层** **传递数据**.参阅[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects).
* 为了在用户界面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层.
* `BookDto`继承自 `AuditedEntityDto<Guid>`.上面定义的 `Book` 实体一样具有一些审计属性.
在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射:
@ -322,12 +327,11 @@ namespace Acme.BookStore
}
````
> 参阅 [对象对象映射](../Object-To-Object-Mapping.md) 文档了解详情.
> 参阅 [对象对象映射](../Object-To-Object-Mapping.md) 文档了解详情.
### CreateUpdateBookDto
在`Acme.BookStore.Application.Contracts`项目中创建一个名为 `CreateUpdateBookDto` 的DTO类:
在`Acme.BookStore.Application.Contracts`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `CreateUpdateBookDto` 的DTO类:
````csharp
using System;
using System.ComponentModel.DataAnnotations;
@ -354,7 +358,7 @@ namespace Acme.BookStore.Books
````
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation).
* 它定义了数据注释特性(如`[Required]`)来定义属性的验证规则. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation).
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射,最终映射配置类如下:
@ -378,7 +382,7 @@ namespace Acme.BookStore
### IBookAppService
下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口:
下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目创建 `Books` 文件夹(命名空间),并在其中添加名为`IBookAppService`的接口:
````csharp
using System;
@ -400,12 +404,12 @@ namespace Acme.BookStore.Books
````
* 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践.
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成).
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定(例如使用不同的DTO进行创建和更新).
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 从这个接口扩展不是必需的,你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成).
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO(例如使用不同的DTO进行创建和更新).
### BookAppService
在`Acme.BookStore.Application`项目中创建名为 `BookAppService``IBookAppService` 实现:
是时候实现`IBookAppService`接口了.在`Acme.BookStore.Application`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `BookAppService` 的类:
````csharp
using System;
@ -439,19 +443,20 @@ namespace Acme.BookStore.Books
### 自动生成API Controllers
通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们.
在典型的ASP.NET Core应用程序中,你创建**API Controller**以将应用程序服务公开为**HTTP API**端点. 这将允许浏览器或第三方客户端通过HTTP调用它们.
ABP可以[**自动**](../API/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器.
ABP可以[**自动**](../API/Auto-API-Controllers.md)按照约定将你的应用程序服务配置为MVC API控制器.
### Swagger UI
启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL.
使用`CTRL+F5`运行应用程序 ({{if UI=="MVC"}}`Acme.BookStore.Web`{{else}}`Acme.BookStore.HttpApi.Host`{{end}})并使用浏览器访问`https://localhost:<port>/swagger/` on your browser. 使用你自己的端口号替换 `<port>`.
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的:
你会看到一些内置的服务端点和`Book`服务,它们都是REST风格的端点:
![bookstore-swagger](images/bookstore-swagger.png)
![bookstore-swagger](./images/bookstore-swagger.png)
Swagger有一个很好的UI来测试API.
Swagger有一个很好的UI来测试API.
你可以尝试执行`[GET] /api/app/book` API来获取书籍列表, 服务端会返回以下JSON结果:

256
docs/zh-Hans/Tutorials/Part-2.md

@ -2,26 +2,10 @@
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
{{
if UI == "MVC"
DB="ef"
DB_Text="Entity Framework Core"
UI_Text="mvc"
else if UI == "NG"
DB="mongodb"
DB_Text="MongoDB"
UI_Text="angular"
else
DB ="?"
UI_Text="?"
end
}}
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
@ -44,16 +28,30 @@ end
## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合:
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=UDNlLiPiBiw&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=2" target="_blank">发布在YouTube</a>**.
{{end}}
{{if UI == "MVC"}}
## 动态JavaScript代理
通常在 **JavaScript** 端通过AJAX调用HTTP API端点. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法.
**JavaScript** 端通过AJAX调用HTTP API端点是常见的做法. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法.
ABP**动态**为所有API端点创建 **[JavaScript代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)**. 所以你可以像调用**Javascript本地方法**一样使用任何**端点**.
@ -79,21 +77,21 @@ acme.bookStore.books.book.getList({}).done(function (result) { console.log(resul
![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png)
Let's **create a new book** using the `create` function:
让我们使用 `create` 函数**创建一本书**:
````js
acme.bookStore.books.book.create({
name: 'Foundation',
type: 7,
publishDate: '1951-05-24',
price: 21.5
}).then(function (result) {
console.log('successfully created the book with id: ' + result.id);
acme.bookStore.books.book.create({
name: 'Foundation',
type: 7,
publishDate: '1951-05-24',
price: 21.5
}).then(function (result) {
console.log('successfully created the book with id: ' + result.id);
});
````
> 如果你下载了本教程的源代码并按照示例中的步骤操作,你需要传递`authorId`参数给创建方法以**创建一本新书**.
您应该在控制台中看到类似以下的消息:
````text
@ -102,7 +100,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
检查数据库中的 `Books` 表你会看到新的一行. 你可以自己尝试使用 `get`, `update``delete` 函数.
我们将利用这些动态代理功能在接下来的章节来与服务器通信.
在接下来的章节,我们将利用这些动态代理函数与服务器通信.
{{end}}
@ -112,7 +110,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
本地化文本位于 `Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹下:
![bookstore-localization-files](./images/bookstore-localization-files-v2.png)
![bookstore-localization-files](images/bookstore-localization-files-v2.png)
打开 `en.json` (*英文翻译*)文件并更改内容,如下所示:
@ -154,7 +152,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
* 为按钮项添加 `Menu:` 前缀.
* 使用 `Enum:<enum-type>:<enum-value>` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.
如果未在本地化文件中定义文本,则文本将**回退**到本地化键(作为ASP.NET Core的标准行为).
如果未在本地化文件中定义文本,则文本将**回退**到本地化键(ASP.NET Core的标准行为).
> ABP本地化系统建立在[ASP.NET Core标准本地化](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization)系统之上,并以多种方式进行了扩展. 有关详细信息请参见[本地化文档](../Localization.md).
@ -162,7 +160,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
## 创建图书页面
是时候创建可见的和可用的东西了! 代替经典的MVC,我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start).
是时候创建可见的和可用的东西了! 我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start),而不是经典的MVC.
`Acme.BookStore.Web` 项目的 `Pages` 文件夹下创建一个名为新的 `Books` 的文件夹. 然后在文件夹右键选择 **添加 > Razor Page** 菜单. 输入名称 `Index`:
@ -189,13 +187,13 @@ namespace Acme.BookStore.Web.Pages.Books
{
public void OnGet()
{
}
}
}
```
### 将Book页面添加到主菜单
### 将图书页面添加到主菜单
打开 `Menus` 文件夹中的 `BookStoreMenuContributor` 类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码:
@ -223,9 +221,7 @@ context.Menu.AddItem(
### 图书列表
We will use the [Datatables.net](https://datatables.net/) jQuery library to show the book list. Datatables library completely work via AJAX, it is fast, popular and provides a good user experience.
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验.
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示图书列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验.
> Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件.
@ -261,7 +257,7 @@ We will use the [Datatables.net](https://datatables.net/) jQuery library to show
`Pages/Books/` 文件夹中创建 `index.js`文件
![bookstore-index-js-file](./images/bookstore-index-js-file-v3.png)
![bookstore-index-js-file](images/bookstore-index-js-file-v3.png)
`index.js` 的内容如下:
@ -321,8 +317,8 @@ $(function () {
````
* `abp.localization.getResource` 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值.
* `abp.libs.datatables.normalizeConfiguration`一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置.
* `abp.libs.datatables.createAjax`帮助ABP的动态JavaScript API代理跟[Datatable](https://datatables.net/)的格式相适应的辅助方法.
* `abp.libs.datatables.normalizeConfiguration`是一个辅助方法.不是必须的, 但是它通过为缺省的选项提供约定的值来简化[Datatables](https://datatables.net/)配置.
* `abp.libs.datatables.createAjax`另一个辅助方法,用来适配ABP的动态JavaScript API代理和[Datatable](https://datatables.net/)期望的参数格式.
* `acme.bookStore.books.book.getList` 是动态JavaScript代理函数(上面已经介绍过了)
* [luxon](https://moment.github.io/luxon/) 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作.
@ -334,15 +330,13 @@ $(function () {
![Book list](images/bookstore-book-list-3.png)
这是一个完全正常工作的服务端分页,排序和本地化的图书列表.
{{end}}
这是一个可以正常工作的,服务端分页,排序和本地化的图书列表.
{{if UI == "NG"}}
## 安装NPM包
>注意: 本教程基于ABP Framework v3.0.3+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md).
>注意: 本教程基于ABP Framework v3.1.0+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md).
`angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包:
@ -357,9 +351,7 @@ yarn
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库.
### BookModule
运行以下命令创建一个名为 `BookModule` 的新模块:
运行以下命令在angular应用程序根目录创建一个名为 `BookModule` 的新模块:
```bash
yarn ng generate module book --module app --routing --route books
@ -417,7 +409,7 @@ const routes: Routes = [
];
````
现在打开 `src/app/route.provider.ts` 以下替换 `configureRoutes` 函数:
现在打开 `src/app/route.provider.ts` 替换 `configureRoutes` 函数为以下代码:
```js
function configureRoutes(routes: RoutesService) {
@ -450,27 +442,31 @@ function configureRoutes(routes: RoutesService) {
`RoutesService` 是ABP框架提供的用于配置主菜单和路由的服务.
* `path` 路由的URL.
* `path` 路由的URL.
* `name` 菜单项的名称(参阅[本地化文档](../UI/Angular/Localization.md)了解更多).
* `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标).
* `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面.
* `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account``eLayoutType.empty`.
* `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标).
* `order` 菜单项的排序.
* `layout` BooksModule路由的布局. (有三个预定义的布局类型: `eLayoutType.application`, `eLayoutType.account``eLayoutType.empty`).
更多信息请参阅[RoutesService 文档](../UI/Angular/Modifying-the-Menu.md#via-routesservice).
更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice).
### 生成服务代理
### 生成代理
[ABP CLI](../CLI.md) 提供 `generate-proxy` 命令为HTTP APIs生成客户端代理.有了这些代理,在客户端使用HTTP APIs变得更加方便. 运行 `generate-proxy` 命令前, 你的 host 必须正在运行.
ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md).
> **警告**: 使用IIS Express时有一个问题; 它不允许从另一个进程连接应用程序. 如果你使用Visual Studio, 在运行按钮的下拉框中选择`Acme.BookStore.HttpApi.Host`,不要选择IIS Express, 如下图:
`angular` 文件夹下运行以下命令:
![vs-run-without-iisexpress](images/vs-run-without-iisexpress.png)
启动host应用程序后,在 `angular` 文件夹下运行以下命令:
```bash
abp generate-proxy
abp generate-proxy -t ng
```
生成的文件如下:
这个命令将在`/src/app/proxy/books`文件夹下产生以下文件:
![Generated files](./images/generated-proxies-2.png)
![Generated files](images/generated-proxies-3.png)
### BookComponent
@ -479,8 +475,7 @@ abp generate-proxy
```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',
@ -494,7 +489,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;
@ -503,8 +498,8 @@ export class BookComponent implements OnInit {
}
```
* 我们注入了生成的 `BookService`.
* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索.
* 我们引入并注入了生成的 `BookService`.
* 我们使用 [ListService](../UI/Angular/List-Service.md),它是一个工具服务,提供了易用的分页,排序和搜索.
打开 `/src/app/book/book.component.html` 用以下内容替换它:
@ -545,9 +540,138 @@ export class BookComponent implements OnInit {
现在你可以在浏览器看到最终结果:
![Book list final result](./images/bookstore-book-list.png)
![图书列表最终结果](images/bookstore-book-list.png)
{{end}}
{{else if UI == "Blazor" || UI == "BlazorServer"}}
## 创建图书页面
是时候创建可见和可用的东西了! 右击`Acme.BookStore.Blazor`项目下的`Pages`文件夹,新建一个名为`Books.razor`的**razor组件**.
![blazor-add-books-component](images/blazor-add-books-component.png)
用以下内容替换这个组件的内容:
````html
@page "/books"
<h2>Books</h2>
@code {
}
````
### 将图书页面添加到主菜单
打开`Blazor`项目中的`BookStoreMenuContributor`类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码:
````csharp
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/books"
)
)
);
````
运行项目,使用用户名 `admin` 和密码 `1q2w3E*` 登录到应用程序. 看到新菜单项已添加到顶部栏:
![blazor-menu-bookstore](images/blazor-menu-bookstore.png)
点击BookStore下的Books子菜单项就会跳转到空的图书页面.
### 图书列表
我们将使用[Blazorise library](https://blazorise.com/)作为UI组件.它是一个强大的库,支持主要的HTML/CSS框架,包括Bootstrap.
ABP提供了一个通用的基类,`AbpCrudPageBase<...>`,用来创建CRUD风格的页面.这个基类兼容用来构建`IBookAppService`的`ICrudAppService`.所以我们从`AbpCrudPageBase`继承,获得标准CRUD的默认实现.
打开`Books.razor` 并把内容修改成下面这样:
````xml
@page "/books"
@using Volo.Abp.Application.Dtos
@using Acme.BookStore.Books
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<BookStoreResource> L
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
<CardHeader>
<h2>@L["Books"]</h2>
</CardHeader>
<CardBody>
<DataGrid TItem="BookDto"
Data="Entities"
ReadData="OnDataGridReadAsync"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Name)"
Caption="@L["Name"]"></DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType:{(int)context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.PublishDate)"
Caption="@L["PublishDate"]">
<DisplayTemplate>
@context.PublishDate.ToShortDateString()
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Price)"
Caption="@L["Price"]">
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.CreationTime)"
Caption="@L["CreationTime"]">
<DisplayTemplate>
@context.CreationTime.ToLongDateString()
</DisplayTemplate>
</DataGridColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
````
> 如果你可以编译并运行成功,但看到一些语法错误.你可以忽略这些错误,因为Visual Studio处理Blazor还有一些bug.
* `AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>`实现了所有的CRUD细节,我们从它继承.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync`定义在基类中.
* 注入`IStringLocalizer<BookStoreResource>` (作为`L`对象),用于本地化.
虽然上面的代码非常容易理解,你仍然可以查看Blazorise [Card](https://blazorise.com/docs/components/card/)和[DataGrid](https://blazorise.com/docs/extensions/datagrid/)文档以更好地理解它们.
#### 关于AbpCrudPageBase
对于图书页面,我们将持续从`AbpCrudPageBase`获得益处. 你可以只注入`IBookAppService`并自己执行所有的服务端调用(感谢ABP的[动态C# HTTP API客户端代理](../API/Dynamic-CSharp-API-Clients.md)系统).
## 运行最终应用程序
你可以运行应用程序!该部分的最终用户界面如下所示:
![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png)
这是一个可以正常工作的,服务端分页,排序和本地化的图书列表.
{{end # UI }}
## 下一章

600
docs/zh-Hans/Tutorials/Part-3.md

@ -2,27 +2,10 @@
````json
//[doc-params]
{
"UI": ["MVC","NG"],
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
@ -45,16 +28,30 @@ end
## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合:
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=TLShZO8u2VE&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=3" target="_blank">发布在YouTube</a>**.
{{end}}
{{if UI == "MVC"}}
## 创建新书籍
通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示:
通过本节, 你将会了解如何创建一个 modal form 实现新增书籍的功能. model dialog将如下图所示:
![bookstore-create-dialog](./images/bookstore-create-dialog-2.png)
@ -66,7 +63,7 @@ end
#### CreateModal.cshtml.cs
打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现:
打开 `CreateModal.cshtml.cs` 代码文件(`CreateModalModel` 类),替换成以下代码:
````C#
using System.Threading.Tasks;
@ -101,10 +98,10 @@ namespace Acme.BookStore.Web.Pages.Books
}
````
* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法.
* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 间接继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法.
* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上.
* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法.
* 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中具有一些默认值分配或代码执行,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值.
* 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中赋值一些默认值或执行一些代码,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值.
#### CreateModal.cshtml
@ -134,10 +131,9 @@ namespace Acme.BookStore.Web.Pages.Books
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单.
* `abp-model` 指定了 `Book` 属性为模型对象.
* `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发.
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要).
> 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态时,我们不希望包括所有布局.
> 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态窗口时,我们不希望包括所有布局.
### 添加 "New book" 按钮
@ -195,9 +191,9 @@ namespace Acme.BookStore.Web.Pages.Books
如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮:
![bookstore-new-book-button](./images/bookstore-new-book-button-2.png)
![bookstore-new-book-button](images/bookstore-new-book-button-2.png)
打开 `Pages/book/index.js``datatable` 配置代码后面添加如下代码:
打开 `Pages/Book/Index.js``datatable` 配置代码后面添加如下代码:
````js
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
@ -212,9 +208,9 @@ $('#NewBookButton').click(function (e) {
});
````
* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节.
* `abp.ModalManager` 是一个在客户端管理modal的辅助类.它内部使用了Twitter Bootstrap的标准modal组件,但通过简化的API抽象了许多细节.
* `createModal.onResult(...)` 用于在创建书籍后刷新数据表格.
* `createModal.open();` 用于打开模态创建新书籍.
* `createModal.open();` 用于打开modal创建新书籍.
`Index.js` 的内容最终如下所示:
@ -335,8 +331,8 @@ namespace Acme.BookStore.Web.Pages.Books
}
````
* `[HiddenInput]``[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值.
* 在 `OnGetAsync` 方法中,`BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性.
* `[HiddenInput]``[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串参数中获取Id的值.
* 在 `OnGetAsync` 方法中, 我们从 `BookAppService` 获得 `BookDto` ,并将它映射成DTO对象 `CreateUpdateBookDto`.
* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体.
### BookDto 到 CreateUpdateBookDto 对象映射
@ -391,10 +387,10 @@ namespace Acme.BookStore.Web
这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点:
* 它包含`id`属性的`abp-input`, 用于存储编辑书的 `id` (它是隐藏的Input)
* 它包含`id`属性的`abp-input`, 用于存储编辑书`id` (它是隐藏的Input)
* 此页面指定的post地址是`Books/EditModal`.
### 为表格添加 "操作(Actions)" 下拉菜单
### 为表格添加 "操作(Actions)" 下拉菜单
我们将为表格每行添加下拉按钮 ("Actions"):
@ -516,9 +512,9 @@ $(function () {
}
````
* `confirmMessage` 用来在实际执行 `action` 前向用户进行确认.
* 通过javascript代理方法 `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求删除一个book实体.
* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息.
* `confirmMessage` 执行 `action` 前向用户进行确认.
* `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求删除一个book.
* `abp.notify.info` 执行删除操作后显示一个通知信息.
由于我们使用了两个新的本地化文本(`BookDeletionConfirmationMessage`和`SuccesslyDeleted`),因此你需要将它们添加到本地化文件(`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下的`en.json`):
@ -640,7 +636,7 @@ $(function () {
## 创建新书籍
下面的章节中,你将学习到如何创建一个新的模态对话框来新增书籍.
下面的章节中,你将学习到如何创建一个新的模态窗口新增书籍.
### BookComponent
@ -649,8 +645,7 @@ $(function () {
```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',
@ -666,7 +661,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;
@ -680,7 +675,8 @@ export class BookComponent implements OnInit {
}
```
* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法.
* 我们定义了一个名为 `isModalOpen` 的属性和 `createBook` 方法.
打开 `/src/app/book/book.component.html` 做以下更改:
@ -690,9 +686,9 @@ export class BookComponent implements OnInit {
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
</div>
</div>
<div class="text-right col col-md-6">
<!-- Add the "new book" button here -->
<div class="text-lg-right pt-2">
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
@ -700,7 +696,7 @@ export class BookComponent implements OnInit {
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>
</button>
</div>
</div>
</div>
</div>
@ -726,11 +722,11 @@ export class BookComponent implements OnInit {
```
* 添加了 `New book` 按钮到卡片头部.
* 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处.
* 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些额外的好处.
你可以打开浏览器,点击**New book**按钮看到模态框.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png)
![Empty modal for new book](images/bookstore-empty-new-book-modal.png)
### 添加响应式表单
@ -741,8 +737,7 @@ export class BookComponent implements OnInit {
```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 { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // add bookTypeOptions
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this
@Component({
@ -756,12 +751,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;
@ -772,7 +763,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;
@ -800,7 +791,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();
@ -809,12 +800,11 @@ export class BookComponent implements OnInit {
}
```
* 导入了 `FormGroup, FormBuilder and Validators`.
* 从` @angular/forms `导入了 `FormGroup, FormBuilder and Validators`.
* 添加了 `form: FormGroup` 变量.
* 添加了 `bookType` 属性,你可以从模板中获取 `BookType` 枚举成员.
* 添加了 `bookTypes` 属性作为 `BookType` 枚举成员列表. 将在表单选项中使用.
* 我们注入了 `fb: FormBuilder` 服务到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 服务为生成控件提供了方便的方法. 它减少了构建复杂表单所需的样板文件的数量.
* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. 该方法创建一个响应式表单去创建新书.
* 我们注入了 `FormBuilder` 到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 提供了简便的方法生成表单控件. 它减少了构建复杂表单所需的样板文件的数量.
* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法.
* 添加了`save` 方法.
打开 `/src/app/book/book.component.html`,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
@ -836,7 +826,7 @@ export class BookComponent implements OnInit {
<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>
@ -897,13 +887,12 @@ export class BookModule { }
* 我们导入了 `NgbDatepickerModule` 来使用日期选择器.
打开 `/src/app/book/book.component.ts` 使用以内容替换:
打开 `/src/app/book/book.component.ts` 使用以内容替换:
```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
@ -923,11 +912,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;
@ -938,7 +923,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;
@ -964,7 +949,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();
@ -974,11 +959,11 @@ export class BookComponent implements OnInit {
```
* 导入了 `NgbDateNativeAdapter``NgbDateAdapter`.
* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview).
* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview).
现在你可以打开浏览器看到以下变化:
![Save button to the modal](./images/bookstore-new-book-form-v2.png)
![Save button to the modal](images/bookstore-new-book-form-v2.png)
## 更新书籍
@ -987,8 +972,7 @@ export class BookComponent implements OnInit {
```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';
@ -1001,15 +985,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;
@ -1020,7 +1000,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;
@ -1028,14 +1008,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;
@ -1061,8 +1041,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;
@ -1074,10 +1054,10 @@ export class BookComponent implements OnInit {
```
* 我们声明了类型为 `BookDto``selectedBook` 变量.
* 我们添加了 `editBook` 方法, 根据给定`Id` 设置 `selectedBook` 对象.
* 我们添加了 `editBook` 方法, 根据给定书 `Id` 设置 `selectedBook` 对象.
* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单.
* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象.
* 我们替换了 `save` 方法.
* 我们修改了 `save` 方法,同时处理新建和更新操作.
### 添加 "Actions" 下拉框到表格
@ -1111,7 +1091,7 @@ export class BookComponent implements OnInit {
在表格的第一列添加了一个 "Actions" 下拉菜单,如下图所示:
![Action buttons](./images/bookstore-actions-buttons.png)
![Action buttons](images/bookstore-actions-buttons.png)
同时如下所示更改 `ng-template #abpHeader` 部分:
@ -1147,13 +1127,13 @@ 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());
}
});
}
```
* 我们入了 `ConfirmationService`.
* 我们入了 `ConfirmationService`.
* 我们注入了 `ConfirmationService` 到构造函数.
* 添加了 `delete` 方法.
@ -1161,6 +1141,7 @@ delete(id: string) {
### 添加删除按钮:
打开 `/src/app/book/book.component.html` 修改 `ngbDropdownMenu` 添加删除按钮:
```html
@ -1174,14 +1155,445 @@ delete(id: string) {
最终操作下拉框UI看起来如下:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png)
![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png)
点击 `delete` 操作调用 `delete` 方法,然后显示一个确认弹层如下图所示.
![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png)
{{end}}
{{if UI == "Blazor" || UI == "BlazorServer"}}
## 创建新书籍
通过本节, 你将会了解如何创建一个模态窗口实现新增书籍的功能. 因为我们已经从 `AbpCrudPageBase` 继承, 所以只需要开发视图部分.
### 添加 "New Button" 按钮
打开 `Books.razor` 替换 `<CardHeader>` 部分为以下代码:
````xml
<CardHeader>
<Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Books"]</h2>
</Column>
<Column ColumnSize="ColumnSize.IsAuto">
<Button Color="Color.Primary"
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
</Column>
</Row>
</CardHeader>
````
如下图所示,卡片头 **右侧** 添加了 **New book** 按钮:
![blazor-add-book-button](images/blazor-add-book-button.png)
现在, 我们可以添加点击按钮后打开的模态窗口了.
### 书籍创建模态窗口
打开 `Books.razor`, 添加以下代码到页面底部:
````xml
<Modal @ref="@CreateModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@L["NewBook"]</ModalTitle>
<CloseButton Clicked="CloseCreateModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="CreateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
这段代码需要一个服务; 在文件顶部, `@inherits...` 行前, 注入 `AbpBlazorMessageLocalizerHelper<T>`:
````csharp
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
````
* 表单实现了验证功能, `AbpBlazorMessageLocalizerHelper` 用于本地化验证消息.
* `CreateModal` 对象, `CloseCreateModalAsync``CreateEntityAsync` 方法定义在基类中. 参阅 [Blazorise文档](https://blazorise.com/docs/) 以深入理解 `Modal` 和其它组件.
这就是全部了. 运行应用程序, 尝试添加一本新书.
![blazor-new-book-modal](images/blazor-new-book-modal.png)
## 更新书籍
编辑书籍与新建书籍很类似.
点击 `delete` 动作调用 `delete` 方法,然后无法显示一个确认弹层如下图所示.
### 操作下拉菜单
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png)
打开 `Books.razor` , 在 `DataGridColumns` 中添加以下 `DataGridEntityActionsColumn` 作为第一项:
````xml
<DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
<DisplayTemplate>
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Clicked="() => OpenEditModalAsync(context)" />
</EntityActions>
</DisplayTemplate>
</DataGridEntityActionsColumn>
````
* `OpenEditModalAsync` 定义在基类中, 它接收实体(书籍)参数, 编辑这个实体.
`DataGridEntityActionsColumn` 组件用于显示 `DataGrid` 每一行中的"操作" 下拉菜单. 如果其中只有唯一的操作, `DataGridEntityActionsColumn` 显示 **唯一按钮**, 而不是下拉菜单.
![blazor-edit-book-action](images/blazor-edit-book-action-2.png)
### 编辑模态窗口
我们现在可以定义一个模态窗口编辑书籍. 加入下面的代码到 `Books.razor` 页面的底部:
````xml
<Modal @ref="@EditModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@EditingEntity.Name</ModalTitle>
<CloseButton Clicked="CloseEditModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@EditingEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="UpdateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
### AutoMapper 配置
基类 `AbpCrudPageBase` 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 系统将 `BookDto` 对象转化为`CreateUpdateBookDto` 对象. 因此, 我们需要定义映射.
打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreBlazorAutoMapperProfile `, 替换成以下内容:
````csharp
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Blazor
{
public class BookStoreBlazorAutoMapperProfile : Profile
{
public BookStoreBlazorAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
}
}
````
* `CreateMap<BookDto, CreateUpdateBookDto>();` 行用于定义映射.
### 测试编辑模态窗口
你可以运行程序并尝试编辑一本书.
![blazor-edit-book-modal](images/blazor-edit-book-modal.png)
> 提示: 尝试保留 *Name* 字段为空并提交表单, 将显示验证错误消息.
## 删除书籍
打开 `Books.razor` 页面, 在 `EntityActions` 中的"编辑" 操作下面加入以下的 `EntityAction`:
````xml
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="() => GetDeleteConfirmationMessage(context)" />
````
* `DeleteEntityAsync` 定义在基类中. 通过向服务器发起请求删除实体.
* `ConfirmationMessage` 执行操作前显示确认消息的回调函数.
* `GetDeleteConfirmationMessage` 定义在基类中. 你可以覆写这个方法 (或传递其它值给 `ConfirmationMessage` 参数) 以定制本地化消息.
因为"操作" 按钮现在有了两个操作, 变成了下拉菜单:
![blazor-edit-book-action](images/blazor-delete-book-action.png)
运行程序并尝试删除一本书.
## 完整的 CRUD UI 代码
下面是完整的创建图书管理CRUD页面的代码, 这些代码在上面是分成两部分开发的:
````xml
@page "/books"
@using Volo.Abp.Application.Dtos
@using Acme.BookStore.Books
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Components.Web
@inject IStringLocalizer<BookStoreResource> L
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
<CardHeader>
<Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Books"]</h2>
</Column>
<Column ColumnSize="ColumnSize.IsAuto">
<Button Color="Color.Primary"
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
</Column>
</Row>
</CardHeader>
<CardBody>
<DataGrid TItem="BookDto"
Data="Entities"
ReadData="OnDataGridReadAsync"
CurrentPage="CurrentPage"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
<DisplayTemplate>
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Clicked="() => OpenEditModalAsync(context)" />
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" />
</EntityActions>
</DisplayTemplate>
</DataGridEntityActionsColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Name)"
Caption="@L["Name"]"></DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType:{(int) context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.PublishDate)"
Caption="@L["PublishDate"]">
<DisplayTemplate>
@context.PublishDate.ToShortDateString()
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Price)"
Caption="@L["Price"]">
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.CreationTime)"
Caption="@L["CreationTime"]">
<DisplayTemplate>
@context.CreationTime.ToLongDateString()
</DisplayTemplate>
</DataGridColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
<Modal @ref="@CreateModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@L["NewBook"]</ModalTitle>
<CloseButton Clicked="CloseCreateModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="CreateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
<Modal @ref="@EditModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@EditingEntity.Name</ModalTitle>
<CloseButton Clicked="CloseEditModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@EditingEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="UpdateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
{{end}}
## 下一章
查看本教程的[下一章](Part-4.md).
查看本教程的[下一章](Part-4.md).

2
docs/zh-Hans/Tutorials/Todo/Index.md

@ -682,7 +682,7 @@ ABP提供了一个便捷的功能来自动创建客户端服务, 以方便地使
当启动 `TodoApp.HttpApi.Host` 项目后, 在`angular`文件夹中打开一个命令行终端并输入以下命令:
````bash
abp generate-proxy
abp generate-proxy -t ng
````
如果一切顺利, 它应该生成如下输出:

BIN
docs/zh-Hans/Tutorials/images/blazor-add-book-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-add-books-component.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
docs/zh-Hans/Tutorials/images/generated-proxies-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

2
docs/zh-Hans/UI/Angular/Service-Proxies.md

@ -15,7 +15,7 @@ ABP CLI 的`generate-proxies` 命令在 `src/app` 文件夹中创建按模块名
在angular应用程序的**根文件夹**中运行以下命令:
```bash
abp generate-proxy
abp generate-proxy -t ng
```
它只为你自己的应用程序的服务创建代理. 不会为你正在使用的应用程序模块的服务创建代理(默认情况下). 有几个选项,参见[CLI文档](../../CLI).

BIN
docs/zh-Hans/images/create-aspnet-core-application.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 41 KiB

11
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs

@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
@ -45,9 +45,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
var cacheKey = CreateCacheKey();
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && !httpContext.WebSockets.IsWebSocketRequest && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
return configuration;
}
@ -58,10 +57,10 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300) //TODO: Should be configurable.
}
}
);
if (httpContext != null && !httpContext.WebSockets.IsWebSocketRequest)
if (httpContext != null)
{
httpContext.Items[cacheKey] = configuration;
}
@ -74,7 +73,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
var cacheKey = CreateCacheKey();
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && !httpContext.WebSockets.IsWebSocketRequest && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
return configuration;
}

54
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs

@ -8,7 +8,8 @@ using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters;
public class AbpRemoteStreamContentModelBinder : IModelBinder
public class AbpRemoteStreamContentModelBinder<TRemoteStreamContent> : IModelBinder
where TRemoteStreamContent: class, IRemoteStreamContent
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
@ -17,7 +18,7 @@ public class AbpRemoteStreamContentModelBinder : IModelBinder
throw new ArgumentNullException(nameof(bindingContext));
}
var postedFiles = new List<IRemoteStreamContent>();
var postedFiles = GetCompatibleCollection<TRemoteStreamContent>(bindingContext);
// If we're at the top level, then use the FieldName (parameter or property name).
// This handles the fact that there will be nothing in the ValueProviders for this parameter
@ -42,7 +43,7 @@ public class AbpRemoteStreamContentModelBinder : IModelBinder
}
object value;
if (bindingContext.ModelType == typeof(IRemoteStreamContent) || bindingContext.ModelType == typeof(RemoteStreamContent))
if (bindingContext.ModelType == typeof(TRemoteStreamContent))
{
if (postedFiles.Count == 0)
{
@ -63,7 +64,7 @@ public class AbpRemoteStreamContentModelBinder : IModelBinder
// Perform any final type mangling needed.
var modelType = bindingContext.ModelType;
if (modelType == typeof(IRemoteStreamContent[]) || modelType == typeof(RemoteStreamContent[]))
if (modelType == typeof(TRemoteStreamContent[]))
{
value = postedFiles.ToArray();
}
@ -91,7 +92,7 @@ public class AbpRemoteStreamContentModelBinder : IModelBinder
private async Task GetFormFilesAsync(
string modelName,
ModelBindingContext bindingContext,
ICollection<IRemoteStreamContent> postedFiles)
ICollection<TRemoteStreamContent> postedFiles)
{
var request = bindingContext.HttpContext.Request;
if (request.HasFormContentType)
@ -108,13 +109,52 @@ public class AbpRemoteStreamContentModelBinder : IModelBinder
if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
{
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream(), file.FileName, file.ContentType, file.Length));
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream(), file.FileName, file.ContentType, file.Length).As<TRemoteStreamContent>());
}
}
}
else if (bindingContext.IsTopLevelObject)
{
postedFiles.Add(new RemoteStreamContent(request.Body, null, request.ContentType, request.ContentLength));
postedFiles.Add(new RemoteStreamContent(request.Body, null, request.ContentType, request.ContentLength).As<TRemoteStreamContent>());
}
}
private static ICollection<T> GetCompatibleCollection<T>(ModelBindingContext bindingContext)
{
var model = bindingContext.Model;
var modelType = bindingContext.ModelType;
// There's a limited set of collection types we can create here.
//
// For the simple cases: Choose List<T> if the destination type supports it (at least as an intermediary).
//
// For more complex cases: If the destination type is a class that implements ICollection<T>, then activate
// an instance and return that.
//
// Otherwise just give up.
if (typeof(T).IsAssignableFrom(modelType))
{
return new List<T>();
}
if (modelType == typeof(T[]))
{
return new List<T>();
}
// Does collection exist and can it be reused?
if (model is ICollection<T> collection && !collection.IsReadOnly)
{
collection.Clear();
return collection;
}
if (modelType.IsAssignableFrom(typeof(List<T>)))
{
return new List<T>();
}
return (ICollection<T>)Activator.CreateInstance(modelType);
}
}

12
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinderProvider.cs

@ -14,12 +14,16 @@ public class AbpRemoteStreamContentModelBinderProvider : IModelBinderProvider
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(IRemoteStreamContent) ||
context.Metadata.ModelType == typeof(RemoteStreamContent) ||
typeof(IEnumerable<IRemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType) ||
if (context.Metadata.ModelType == typeof(RemoteStreamContent) ||
typeof(IEnumerable<RemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType))
{
return new AbpRemoteStreamContentModelBinder();
return new AbpRemoteStreamContentModelBinder<RemoteStreamContent>();
}
if (context.Metadata.ModelType == typeof(IRemoteStreamContent) ||
typeof(IEnumerable<IRemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType))
{
return new AbpRemoteStreamContentModelBinder<IRemoteStreamContent>();
}
return null;

16
framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/AbpAspNetCoreServiceCollectionExtensions.cs

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Hosting;
using System.Linq;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Microsoft.Extensions.DependencyInjection;
@ -6,6 +8,16 @@ public static class AbpAspNetCoreServiceCollectionExtensions
{
public static IWebHostEnvironment GetHostingEnvironment(this IServiceCollection services)
{
return services.GetSingletonInstance<IWebHostEnvironment>();
var hostingEnvironment = services.GetSingletonInstanceOrNull<IWebHostEnvironment>();
if (hostingEnvironment == null)
{
return new EmptyHostingEnvironment()
{
EnvironmentName = Environments.Development
};
}
return hostingEnvironment;
}
}

19
framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/EmptyHostingEnvironment.cs

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.Extensions.DependencyInjection;
internal class EmptyHostingEnvironment : IWebHostEnvironment
{
public string EnvironmentName { get; set; }
public string ApplicationName { get; set; }
public string WebRootPath { get; set; }
public IFileProvider WebRootFileProvider { get; set; }
public string ContentRootPath { get; set; }
public IFileProvider ContentRootFileProvider { get; set; }
}

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

@ -41,29 +41,28 @@ public class AbpCliCoreModule : AbpModule
Configure<AbpCliOptions>(options =>
{
//TODO: Define constants like done for GenerateProxyCommand.Name.
options.Commands["help"] = typeof(HelpCommand);
options.Commands["prompt"] = typeof(PromptCommand);
options.Commands["new"] = typeof(NewCommand);
options.Commands["get-source"] = typeof(GetSourceCommand);
options.Commands["update"] = typeof(UpdateCommand);
options.Commands["add-package"] = typeof(AddPackageCommand);
options.Commands["add-module"] = typeof(AddModuleCommand);
options.Commands["list-modules"] = typeof(ListModulesCommand);
options.Commands["login"] = typeof(LoginCommand);
options.Commands["login-info"] = typeof(LoginInfoCommand);
options.Commands["logout"] = typeof(LogoutCommand);
options.Commands[HelpCommand.Name] = typeof(HelpCommand);
options.Commands[PromptCommand.Name] = typeof(PromptCommand);
options.Commands[NewCommand.Name] = typeof(NewCommand);
options.Commands[GetSourceCommand.Name] = typeof(GetSourceCommand);
options.Commands[UpdateCommand.Name] = typeof(UpdateCommand);
options.Commands[AddPackageCommand.Name] = typeof(AddPackageCommand);
options.Commands[AddModuleCommand.Name] = typeof(AddModuleCommand);
options.Commands[ListModulesCommand.Name] = typeof(ListModulesCommand);
options.Commands[LoginCommand.Name] = typeof(LoginCommand);
options.Commands[LoginInfoCommand.Name] = typeof(LoginInfoCommand);
options.Commands[LogoutCommand.Name] = typeof(LogoutCommand);
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);
options.Commands["switch-to-nightly"] = typeof(SwitchToNightlyCommand);
options.Commands["translate"] = typeof(TranslateCommand);
options.Commands["build"] = typeof(BuildCommand);
options.Commands["bundle"] = typeof(BundleCommand);
options.Commands["create-migration-and-run-migrator"] = typeof(CreateMigrationAndRunMigratorCommand);
options.Commands["install-libs"] = typeof(InstallLibsCommand);
options.Commands[SuiteCommand.Name] = typeof(SuiteCommand);
options.Commands[SwitchToPreviewCommand.Name] = typeof(SwitchToPreviewCommand);
options.Commands[SwitchToStableCommand.Name] = typeof(SwitchToStableCommand);
options.Commands[SwitchToNightlyCommand.Name] = typeof(SwitchToNightlyCommand);
options.Commands[TranslateCommand.Name] = typeof(TranslateCommand);
options.Commands[BuildCommand.Name] = typeof(BuildCommand);
options.Commands[BundleCommand.Name] = typeof(BundleCommand);
options.Commands[CreateMigrationAndRunMigratorCommand.Name] = typeof(CreateMigrationAndRunMigratorCommand);
options.Commands[InstallLibsCommand.Name] = typeof(InstallLibsCommand);
});
Configure<AbpCliServiceProxyOptions>(options =>

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs

@ -14,6 +14,8 @@ namespace Volo.Abp.Cli.Commands;
public class AddModuleCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "add-module";
private AddModuleInfoOutput _lastAddedModuleInfo;
public ILogger<AddModuleCommand> Logger { get; set; }

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

@ -14,6 +14,8 @@ namespace Volo.Abp.Cli.Commands;
public class AddPackageCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "add-package";
public ILogger<AddPackageCommand> Logger { get; set; }
protected ProjectNugetPackageAdder ProjectNugetPackageAdder { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs

@ -12,6 +12,8 @@ namespace Volo.Abp.Cli.Commands;
public class BuildCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "build";
public IDotNetProjectDependencyFiller DotNetProjectDependencyFiller { get; set; }
public IChangedProjectFinder ChangedProjectFinder { get; set; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BundleCommand.cs

@ -11,6 +11,8 @@ namespace Volo.Abp.Cli.Commands;
public class BundleCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "bundle";
public ILogger<BundleCommand> Logger { get; set; }
public IBundlingService BundlingService { get; set; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CreateMigrationAndRunMigratorCommand.cs

@ -12,6 +12,8 @@ namespace Volo.Abp.Cli.Commands;
public class CreateMigrationAndRunMigratorCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "create-migration-and-run-migrator";
public ICmdHelper CmdHelper { get; }
public ILogger<CreateMigrationAndRunMigratorCommand> Logger { get; set; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GetSourceCommand.cs

@ -13,6 +13,8 @@ namespace Volo.Abp.Cli.Commands;
public class GetSourceCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "get-source";
private readonly SourceCodeDownloadService _sourceCodeDownloadService;
public ModuleProjectBuilder ModuleProjectBuilder { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs

@ -12,6 +12,8 @@ namespace Volo.Abp.Cli.Commands;
public class HelpCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "help";
public ILogger<HelpCommand> Logger { get; set; }
protected AbpCliOptions AbpCliOptions { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/InstallLibsCommand.cs

@ -12,6 +12,8 @@ namespace Volo.Abp.Cli.Commands;
public class InstallLibsCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "install-libs";
public ILogger<InstallLibsCommand> Logger { get; set; }
protected IInstallLibsService InstallLibsService { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs

@ -12,6 +12,8 @@ namespace Volo.Abp.Cli.Commands;
public class ListModulesCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "list-modules";
public ModuleInfoProvider ModuleInfoProvider { get; }
public ILogger<ListModulesCommand> Logger { get; set; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginCommand.cs

@ -18,6 +18,8 @@ namespace Volo.Abp.Cli.Commands;
public class LoginCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "login";
public ILogger<LoginCommand> Logger { get; set; }
protected AuthService AuthService { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs

@ -11,6 +11,8 @@ namespace Volo.Abp.Cli.Commands;
public class LoginInfoCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "login-info";
public ILogger<LoginInfoCommand> Logger { get; set; }
protected AuthService AuthService { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LogoutCommand.cs

@ -9,6 +9,8 @@ namespace Volo.Abp.Cli.Commands;
public class LogoutCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "logout";
public ILogger<LogoutCommand> Logger { get; set; }
protected AuthService AuthService { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs

@ -19,6 +19,8 @@ namespace Volo.Abp.Cli.Commands;
public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransientDependency
{
public const string Name = "new";
public ILogger<NewCommand> Logger { get; set; }
protected TemplateProjectBuilder TemplateProjectBuilder { get; }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/PromptCommand.cs

@ -7,6 +7,8 @@ namespace Volo.Abp.Cli.Commands;
public class PromptCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "prompt";
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
return Task.CompletedTask;

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Services/ConnectionStringProvider.cs

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.DependencyInjection;
@ -21,7 +22,8 @@ public class ConnectionStringProvider : ITransientDependency
case DatabaseManagementSystem.OracleDevart:
return "Data Source=MyProjectName;Integrated Security=yes;";
case DatabaseManagementSystem.SQLite:
return $"Data Source={Path.Combine(outputFolder, "MyProjectName.db")};".Replace("\\", "\\\\");
var comment = outputFolder.IsNullOrWhiteSpace() ? "//You need to change to an absolute filename" : string.Empty;
return $"Data Source={Path.Combine(outputFolder, "MyProjectName.db")};".Replace("\\", "\\\\") + comment;
default:
return null;
}

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

@ -15,6 +15,8 @@ namespace Volo.Abp.Cli.Commands;
public class SuiteCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "suite";
public ICmdHelper CmdHelper { get; }
private readonly AbpNuGetIndexUrlService _nuGetIndexUrlService;
private readonly NuGetService _nuGetService;

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToNightlyCommand.cs

@ -8,6 +8,8 @@ namespace Volo.Abp.Cli.Commands;
public class SwitchToNightlyCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "switch-to-nightly";
private readonly PackagePreviewSwitcher _packagePreviewSwitcher;
public SwitchToNightlyCommand(PackagePreviewSwitcher packagePreviewSwitcher)

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToPreviewCommand.cs

@ -8,6 +8,8 @@ namespace Volo.Abp.Cli.Commands;
public class SwitchToPreviewCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "switch-to-preview";
private readonly PackagePreviewSwitcher _packagePreviewSwitcher;
public SwitchToPreviewCommand(PackagePreviewSwitcher packagePreviewSwitcher)

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchToStableCommand.cs

@ -8,6 +8,8 @@ namespace Volo.Abp.Cli.Commands;
public class SwitchToStableCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "switch-to-stable";
private readonly PackagePreviewSwitcher _packagePreviewSwitcher;
public SwitchToStableCommand(PackagePreviewSwitcher packagePreviewSwitcher)

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/TranslateCommand.cs

@ -15,6 +15,8 @@ namespace Volo.Abp.Cli.Commands;
public class TranslateCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "translate";
public ILogger<TranslateCommand> Logger { get; set; }
public Task ExecuteAsync(CommandLineArgs commandLineArgs)

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs

@ -13,6 +13,8 @@ namespace Volo.Abp.Cli.Commands;
public class UpdateCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "update";
public ILogger<UpdateCommand> Logger { get; set; }
private readonly VoloNugetPackagesVersionUpdater _nugetPackagesVersionUpdater;

4
framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj

@ -19,8 +19,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Devart.Data.Oracle.EFCore" Version="9.14.1369" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftPackageVersion)" />
<PackageReference Include="Devart.Data.Oracle.EFCore" Version="9.15.1410" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
</ItemGroup>
</Project>

6
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json

@ -46,8 +46,8 @@
"GoHomePage": "返回首頁",
"GoBack": "返回",
"Search": "查詢",
"ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!",
"ItemWillBeDeletedMessage": "This item will be deleted!",
"ManageYourAccount": "Manage your account"
"ItemWillBeDeletedMessageWithFormat": "{0} 將被刪除!",
"ItemWillBeDeletedMessage": "此項目將被刪除!",
"ManageYourAccount": "管理個人帳號"
}
}

66
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs

@ -59,10 +59,10 @@ public class PersonAppServiceClientProxy_Tests : AbpHttpClientTestBase
var id2 = Guid.NewGuid();
var @params = await _peopleAppService.GetParams(new List<Guid>
{
id1,
id2
}, new[] { "name1", "name2" });
{
id1,
id2
}, new[] { "name1", "name2" });
@params.ShouldContain(id1.ToString("N"));
@params.ShouldContain(id2.ToString("N"));
@ -224,10 +224,10 @@ public class PersonAppServiceClientProxy_Tests : AbpHttpClientTestBase
memoryStream2.Position = 0;
var result = await _peopleAppService.UploadMultipleAsync(new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream, "File1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "File2.rtf", "application/rtf2")
});
{
new RemoteStreamContent(memoryStream, "File1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "File2.rtf", "application/rtf2")
});
result.ShouldBe("File1:application/rtf:File1.rtfFile2:application/rtf2:File2.rtf");
}
@ -263,11 +263,11 @@ public class PersonAppServiceClientProxy_Tests : AbpHttpClientTestBase
var result = await _peopleAppService.CreateMultipleFileAsync(new CreateMultipleFileInput()
{
Name = "123.rtf",
Contents = new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream, "1-1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "1-2.rtf", "application/rtf2")
},
Contents = new List<RemoteStreamContent>()
{
new RemoteStreamContent(memoryStream, "1-1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "1-2.rtf", "application/rtf2")
},
Inner = new CreateFileInput()
{
Name = "789.rtf",
@ -283,18 +283,18 @@ public class PersonAppServiceClientProxy_Tests : AbpHttpClientTestBase
var result = await _peopleAppService.GetParamsFromQueryAsync(new GetParamsInput()
{
NameValues = new List<GetParamsNameValue>()
{
new GetParamsNameValue()
{
new GetParamsNameValue()
{
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
},
NameValue = new GetParamsNameValue()
{
Name = "name3",
@ -310,18 +310,18 @@ public class PersonAppServiceClientProxy_Tests : AbpHttpClientTestBase
var result = await _peopleAppService.GetParamsFromFormAsync(new GetParamsInput()
{
NameValues = new List<GetParamsNameValue>()
{
new GetParamsNameValue()
{
new GetParamsNameValue()
{
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
},
NameValue = new GetParamsNameValue()
{
Name = "name3",

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateFileInput.cs

@ -6,5 +6,5 @@ public class CreateFileInput
{
public string Name { get; set; }
public RemoteStreamContent Content { get; set; }
public IRemoteStreamContent Content { get; set; }
}

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateMultipleFileInput.cs

@ -7,7 +7,7 @@ public class CreateMultipleFileInput
{
public string Name { get; set; }
public IEnumerable<IRemoteStreamContent> Contents { get; set; }
public IEnumerable<RemoteStreamContent> Contents { get; set; }
public CreateFileInput Inner { get; set; }
}

29
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hant.json

@ -1,7 +1,7 @@
{
"culture": "zh-Hant",
"texts": {
"AddSubMenuItem": "添加子菜單項",
"AddSubMenuItem": "添加子選單項目",
"AreYouSure": "你確定嗎?",
"BlogDeletionConfirmationMessage": "部落格 '{0}' 將被刪除. 你確定嗎?",
"BlogFeatureNotAvailable": "目前此功能不可使用. 請用 `GlobalFeatureManager` 來啟用它.",
@ -47,15 +47,15 @@
"ExportCSV": "匯出 CSV",
"Features": "功能",
"GenericDeletionConfirmationMessage": "你確定刪除 '{0}' 嗎?",
"IsActive": "積極的",
"IsActive": "是否啟用",
"LastModification": "最後一次修改",
"LastModificationTime": "最後修改時間",
"LoginToAddComment": "登錄後添加評論",
"LoginToRate": "登錄後進行評分",
"LoginToReact": "登錄以作出反應",
"LoginToReply": "登錄後進行回覆",
"MainMenu": "主單",
"MakeMainMenu": "製作主單",
"MainMenu": "主單",
"MakeMainMenu": "製作主單",
"Menu:CMS": "CMS",
"Menus": "菜單",
"MenuDeletionConfirmationMessage": "菜單“{0}”將被刪除。你確定嗎?",
@ -64,10 +64,10 @@
"MenuItems": "菜單項",
"Message": "消息",
"MessageDeletionConfirmationMessage": "這條評論將被完全刪除",
"NewBlog": "新博客",
"NewBlogPost": "新文",
"NewMenu": "新單",
"NewMenuItem": "新的根菜單項",
"NewBlog": "新部落格",
"NewBlogPost": "新部落格貼文",
"NewMenu": "新單",
"NewMenuItem": "新的根選單",
"NewPage": "新的一頁",
"NewTag": "新標籤",
"NoMenuItems": "還沒有菜單項!",
@ -104,9 +104,9 @@
"Permission:MenuManagement.Delete": "刪除",
"Permission:MenuManagement.Update": "更新",
"Permission:Menus": "Menu Management",
"Permission:Menus.Create": "Create",
"Permission:Menus.Delete": "Delete",
"Permission:Menus.Update": "Update",
"Permission:Menus.Create": "創建",
"Permission:Menus.Delete": "刪除",
"Permission:Menus.Update": "更新",
"Permission:PageManagement": "頁面管理",
"Permission:PageManagement:Create": "創建",
"Permission:PageManagement:Delete": "刪除",
@ -150,7 +150,7 @@
"Update": "更新",
"UpdatePreferenceSuccessMessage": "您的 preferences 已經保存",
"UpdateYourEmailPreferences": "更新你的郵件preferences",
"UnMakeMainMenu": "取消主單",
"UnMakeMainMenu": "取消主單",
"UploadFailedMessage": "上傳失敗",
"UserId": "用戶Id",
"Username": "用戶名稱",
@ -158,6 +158,7 @@
"YourEmailAddress": "你的郵件地址",
"YourFullName": "你的全名",
"YourMessage": "你的消息",
"YourReply": "你的回覆"
"YourReply": "你的回覆",
"MarkdownSupported": "支援 <a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a> ."
}
}
}

2
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs

@ -5,6 +5,7 @@ using IdentityModel;
using IdentityServer4.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Volo.Abp.Security.Claims;
namespace Volo.Abp.IdentityServer;
@ -20,6 +21,7 @@ public class AbpClaimsService : DefaultClaimsService
AbpClaimTypes.ImpersonatorUserId,
AbpClaimTypes.Name,
AbpClaimTypes.SurName,
JwtRegisteredClaimNames.UniqueName,
JwtClaimTypes.PreferredUserName,
JwtClaimTypes.GivenName,
JwtClaimTypes.FamilyName,

7
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpUserClaimsFactory.cs

@ -1,10 +1,11 @@
using System;
using System;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.JsonWebTokens;
using Volo.Abp.DependencyInjection;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -41,6 +42,10 @@ public class AbpUserClaimsFactory<TUser> : IUserClaimsPrincipalFactory<TUser>
{
identity.RemoveClaim(usernameClaim);
identity.AddIfNotContains(new Claim(JwtClaimTypes.PreferredUserName, username));
//https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/1627
//https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/05e02b5e0383be40e45c667c12f6667d38e33fcc/src/System.IdentityModel.Tokens.Jwt/ClaimTypeMapping.cs#L52
identity.AddIfNotContains(new Claim(JwtRegisteredClaimNames.UniqueName, username));
}
if (!identity.HasClaim(x => x.Type == JwtClaimTypes.Name))

2
nupkg/common.ps1

@ -136,7 +136,7 @@ $projects = (
"framework/src/Volo.Abp.EntityFrameworkCore",
"framework/src/Volo.Abp.EntityFrameworkCore.MySQL",
"framework/src/Volo.Abp.EntityFrameworkCore.Oracle",
# "framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart",
"framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart",
"framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql",
"framework/src/Volo.Abp.EntityFrameworkCore.Sqlite",
"framework/src/Volo.Abp.EntityFrameworkCore.SqlServer",

2
test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj

@ -17,7 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\..\framework\src\Volo.Abp.Ddd.Domain\Volo.Abp.Ddd.Domain.csproj" />
<ProjectReference Include="..\..\..\framework\src\Volo.Abp.EventBus.Boxes\Volo.Abp.EventBus.Boxes.csproj" />
<ProjectReference Include="..\..\..\framework\src\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
</ItemGroup>
</Project>

10
test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs

@ -5,7 +5,7 @@ using StackExchange.Redis;
using Volo.Abp.Autofac;
using Volo.Abp.Domain;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus;
using Volo.Abp.Modularity;
namespace DistDemoApp
@ -13,8 +13,8 @@ namespace DistDemoApp
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpDddDomainModule),
typeof(AbpEventBusBoxesModule)
)]
typeof(AbpEventBusModule)
)]
public class DistDemoAppSharedModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
@ -28,7 +28,7 @@ namespace DistDemoApp
options.EtoMappings.Add<TodoItem, TodoItemEto>();
options.AutoEventSelectors.Add<TodoItem>();
});
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
@ -36,4 +36,4 @@ namespace DistDemoApp
});
}
}
}
}

Loading…
Cancel
Save