Browse Source

Merge branch 'dev' of https://github.com/abpframework/abp into fix/circular-dependency

pull/3821/head^2
mehmet-erim 6 years ago
parent
commit
706cc6ea54
  1. 24
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  2. 6
      common.DotSettings
  3. 4
      docs/en/Tutorials/Part-1.md
  4. 4
      docs/en/Tutorials/Part-3.md
  5. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png
  6. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png
  7. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png
  8. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png
  9. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png
  10. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png
  11. BIN
      docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png
  12. 1079
      docs/zh-Hans/Tutorials/Part-1.md
  13. 1350
      docs/zh-Hans/Tutorials/Part-2.md
  14. 199
      docs/zh-Hans/Tutorials/Part-3.md
  15. BIN
      docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png
  16. 0
      docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png
  17. 0
      docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png
  18. 0
      docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png
  19. BIN
      docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png
  20. 0
      docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png
  21. BIN
      docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png
  22. BIN
      docs/zh-Hans/Tutorials/images/bookstore-book-list.png
  23. 0
      docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png
  24. 0
      docs/zh-Hans/Tutorials/images/bookstore-books-table.png
  25. BIN
      docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png
  26. 0
      docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png
  27. 0
      docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png
  28. BIN
      docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png
  29. BIN
      docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png
  30. BIN
      docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png
  31. BIN
      docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png
  32. BIN
      docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png
  33. BIN
      docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png
  34. BIN
      docs/zh-Hans/Tutorials/images/bookstore-edit-button.png
  35. BIN
      docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png
  36. BIN
      docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png
  37. BIN
      docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png
  38. 0
      docs/zh-Hans/Tutorials/images/bookstore-homepage.png
  39. 0
      docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png
  40. BIN
      docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png
  41. BIN
      docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png
  42. BIN
      docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png
  43. 0
      docs/zh-Hans/Tutorials/images/bookstore-menu-items.png
  44. BIN
      docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png
  45. BIN
      docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png
  46. BIN
      docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png
  47. BIN
      docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png
  48. BIN
      docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png
  49. BIN
      docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png
  50. BIN
      docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png
  51. BIN
      docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png
  52. 0
      docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png
  53. BIN
      docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png
  54. BIN
      docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png
  55. 0
      docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png
  56. BIN
      docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png
  57. BIN
      docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png
  58. BIN
      docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png
  59. BIN
      docs/zh-Hans/Tutorials/images/bookstore-swagger.png
  60. 0
      docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png
  61. 0
      docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png
  62. BIN
      docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png
  63. 0
      docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png
  64. BIN
      docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png
  65. BIN
      docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png
  66. 0
      docs/zh-Hans/Tutorials/images/bookstore-user-management.png
  67. BIN
      docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png
  68. BIN
      docs/zh-Hans/Tutorials/images/generate-proxy-command.png
  69. BIN
      docs/zh-Hans/Tutorials/images/generated-proxies.png
  70. BIN
      docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png
  71. 9
      docs/zh-Hans/UI/Angular/Config-State.md
  72. 197
      docs/zh-Hans/UI/Angular/Modifying-the-Menu.md
  73. BIN
      docs/zh-Hans/UI/Angular/images/navigation-menu-after-patching.png
  74. BIN
      docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png
  75. BIN
      docs/zh-Hans/UI/Angular/images/navigation-menu-via-app-routing.png
  76. BIN
      docs/zh-Hans/UI/Angular/images/navigation-menu-via-config-state.png
  77. 4
      docs/zh-Hans/docs-nav.json
  78. 16
      framework/Volo.Abp.sln
  79. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl-PL.json
  80. 56
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs
  81. 39
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/AbpIoApiKeyService.cs
  82. 97
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs
  83. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs
  84. 62
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  85. 27
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
  86. 14
      framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs
  87. 9
      framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj
  88. 51
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/AbpEmailingModule.cs
  89. 18
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/AbpEmailTemplateOptions.cs
  90. 24
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplateProvider.cs
  91. 1
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message/en.tpl
  92. 42
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplate.cs
  93. 22
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateContributorList.cs
  94. 36
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinition.cs
  95. 38
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionContext.cs
  96. 22
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionDictionary.cs
  97. 74
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionManager.cs
  98. 9
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionProvider.cs
  99. 18
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateInitializationContext.cs
  100. 121
      framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateProvider.cs

24
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -13,10 +13,11 @@
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:Create": "Create",
"Permission:Accounting": "Accounting",
"Permission:Accounting:Quotation": "Quotation",
"Permission:Accounting": "Accounting",
"Permission:Accounting:Quotation": "Quotation",
"Permission:Accounting:Invoice": "Invoice",
"Menu:Organizations": "Organizations",
"Menu:Accounting": "Accounting",
"Menu:Accounting": "Accounting",
"Menu:Packages": "Packages",
"NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?",
"NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?",
@ -103,8 +104,21 @@
"Price": "Price",
"DiscountText": "Discount text",
"DiscountQuantity": "Discount quantity",
"DiscountPrice": "Discount price",
"DiscountPrice": "Discount price",
"Quotation": "Quotation",
"Generate": "Generate"
"ExtraText": "Extra Text",
"ExtraAmount": "Extra Amount",
"DownloadQuotation": "Download Quotation",
"Invoice": "Invoice",
"TaxNumber": "Tax Number",
"InvoiceNumber": "Invoice Number",
"InvoiceDate": "Invoice Date",
"Quantity": "Quantity",
"AddProduct": "Add Product",
"AddProductWarning": "You need to add product!",
"TotalPrice": "Total Price",
"Generate": "Generate",
"MissingQuantityField": "The quantity field is required!",
"MissingPriceField": "The Price field is required!"
}
}

6
common.DotSettings

@ -20,5 +20,11 @@
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Async/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Generate/=Overrides/Options/=Mutable/@EntryIndexedValue">False</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SQL/@EntryIndexedValue">SQL</s:String>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/HideTypeNameHintsWhenTypeNameIsEvidentFromVariableName/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowMethodReturnTypeNameHints/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForImplicitlyTypedVariables/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForLambdaExpressionParameters/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForLinqQueryRangeVariables/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TypeNameHintsOptions/ShowTypeNameHintsForPatternMatchingExpressions/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Volo/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

4
docs/en/Tutorials/Part-1.md

@ -61,7 +61,7 @@ After creating the project, you need to apply the initial migrations and create
To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}}
Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application
Further information, see the [running the application section](../Getting-Started?UI={{UI}}#run-the-application).
![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png)
@ -335,7 +335,7 @@ INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
### Create the application service
The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
The next step is to create an [application service](../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces.
* `Acme.BookStore.Application` contains the implementations of your application services.

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

@ -96,9 +96,9 @@ namespace Acme.BookStore
````
* `IRepository<Book, Guid>` is injected and used it in the `SeedAsync` to create two book entities as the test data.
* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](../Guid-Generation.md).
### Testing the application service BookAppService
* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation).
### Testing the application service BookAppService
Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

BIN
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

@ -1 +1,198 @@
TODO ....
## ASP.NET Core {{UI_Value}} 教程 - 第三章
````json
//[doc-params]
{
"UI": ["MVC","NG"]
}
````
{{
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
}}
### 关于本教程
这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章:
- [Part-1: 创建项目和书籍列表页面](Part-1.md)
- [Part 2: 创建,编辑,删除书籍](Part-2.md)
- **Part-3: 集成测试(本章)**
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application).
### 解决方案中的测试项目
解决方案中有多个测试项目:
![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png)
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试:
* [xunit](https://xunit.github.io/) 作为主测试框架.
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库.
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库.
### 添加测试数据
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试.
更改`BookStoreTestDataSeedContributor`类如下所示:
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
namespace Acme.BookStore
{
public class BookStoreTestDataSeedContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public BookStoreTestDataSeedContributor(
IRepository<Book, Guid> bookRepository,
IGuidGenerator guidGenerator)
{
_bookRepository = bookRepository;
_guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book(id: _guidGenerator.Create(),
name: "Test book 1",
type: BookType.Fantastic,
publishDate: new DateTime(2015, 05, 24),
price: 21
)
);
await _bookRepository.InsertAsync(
new Book(id: _guidGenerator.Create(),
name: "Test book 2",
type: BookType.Science,
publishDate: new DateTime(2014, 02, 11),
price: 15
)
);
}
}
}
````
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据.
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息).
### 测试 BookAppService
`Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类:
````csharp
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Microsoft.EntityFrameworkCore.Internal;
namespace Acme.BookStore
{
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
//Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test book 1");
}
}
}
````
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查.
新增测试方法,用以测试创建一个合法book实体的场景:
````C#
[Fact]
public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New test book 42");
}
````
新增测试方法,用以测试创建一个非法book实体失败的场景:
````csharp
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<Volo.Abp.Validation.AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
````
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常.
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试:
![bookstore-appservice-tests](./images/bookstore-appservice-tests.png)
恭喜, 绿色图标表示测试已成功通过!

BIN
docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png → docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png → docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png → docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png → docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png → docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png → docs/zh-Hans/Tutorials/images/bookstore-books-table.png

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png → docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png → docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-edit-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png → docs/zh-Hans/Tutorials/images/bookstore-homepage.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png → docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png → docs/zh-Hans/Tutorials/images/bookstore-menu-items.png

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png → docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png → docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-swagger.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png → docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png → docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png → docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

0
docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png → docs/zh-Hans/Tutorials/images/bookstore-user-management.png

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/zh-Hans/Tutorials/images/generate-proxy-command.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

9
docs/zh-Hans/UI/Angular/Config-State.md

@ -236,7 +236,7 @@ const newRoute: ABP.Route = {
path: "page",
invisible: false,
order: 2,
requiredPolicy: "MyProjectName::MyNewPage"
requiredPolicy: "MyProjectName.MyNewPage"
};
this.config.dispatchAddRoute(newRoute);
@ -248,16 +248,17 @@ this.config.dispatchAddRoute(newRoute);
如果你想要**添加一个子路由,你可以这样做:**
```js
import { eIdentityRouteNames } from '@abp/ng.identity';
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
parentName: "AbpAccount::Login",
parentName: eIdentityRouteNames.IdentityManagement,
name: "My New Page",
iconClass: "fa fa-dashboard",
path: "page",
invisible: false,
order: 2,
requiredPolicy: "MyProjectName::MyNewPage"
requiredPolicy: "MyProjectName.MyNewPage"
};
this.config.dispatchAddRoute(newRoute);
@ -291,4 +292,4 @@ this.config.dispatchSetEnvironment({
## 下一步是什么?
* [组件替换](./Component-Replacement.md)
- [修改菜单](./Modifying-the-Menu.md)

197
docs/zh-Hans/UI/Angular/Modifying-the-Menu.md

@ -0,0 +1,197 @@
# 修改菜单
菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局.
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.-->
## 如何添加Logo
环境变量中的 `logoUrl` 是logo的url.
你可以在 `src/assets` 文件夹下添加logo并设置 `logoUrl`:
```js
export const environment = {
// other configurations
application: {
name: 'MyProjectName',
logoUrl: 'assets/logo.png',
},
// other configurations
};
```
## 如何添加导航元素
### 通过 AppRoutingModule 中的 `routes` 属性
你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上.
你可以像以下一样添加 `routes` 属性:
```js
{
path: 'your-path',
data: {
routes: {
name: 'Your navigation',
order: 3,
iconClass: 'fas fa-question-circle',
requiredPolicy: 'permission key here',
children: [
{
path: 'child',
name: 'Your child navigation',
order: 1,
requiredPolicy: 'permission key here',
},
],
} as ABP.Route, // can be imported from @abp/ng.core
}
}
```
- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象.
- `order` 排序导航元素.
- `iconClass``i` 标签的类,在导航标签的左侧.
- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./Permission-Management.md)
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property.
- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`.
添加了上面描述的route属性后,导航菜单如下图所示:
![navigation-menu-via-app-routing](./images/navigation-menu-via-app-routing.png)
## 通过 ConfigState
`ConfigStateService``dispatchAddRoute` 方法可以向菜单添加一个新的导航元素.
```js
// this.config is instance of ConfigStateService
const newRoute: ABP.Route = {
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 2,
requiredPolicy: 'MyProjectName.MyNewPage',
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
`newRoute` 放在根级别,没有任何父路由,url将为`/path`.
如果你想 **添加子路由, 你可以这样做:**
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRoute: ABP.Route = {
parentName: eIdentityRouteNames.IdentityManagement,
name: 'My New Page',
iconClass: 'fa fa-dashboard',
path: 'page',
invisible: false,
order: 3,
requiredPolicy: 'MyProjectName.MyNewPage'
} as Omit<ABP.Route, 'children'>;
this.config.dispatchAddRoute(newRoute);
// returns a state stream which emits after dispatch action is complete
```
`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`.
新路由看起来像这样:
![navigation-menu-via-config-state](./images/navigation-menu-via-config-state.png)
## 如何修改一个导航元素
`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置.
```js
// this.config is instance of ConfigStateService
// eIdentityRouteNames enum can be imported from @abp/ng.identity
const newRouteConfig: Partial<ABP.Route> = {
iconClass: 'fas fa-home',
parentName: eIdentityRouteNames.Administration,
order: 0,
children: [
{
name: 'Dashboard',
path: 'dashboard',
},
],
};
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig);
// returns a state stream which emits after dispatch action is complete
```
* 根据给定的 `parentName`_Home_ 导航移动到 _Administration_ 下拉框下.
* 添加了 icon.
* 指定了顺序.
* 添加了名为 _Dashboard_ 的子路由.
修改后,导航元素看起来像这样:
![navigation-menu-after-patching](./images/navigation-menu-after-patching.png)
## 如何在菜单的右侧添加元素
右侧的元素存储在 @abp/ng.theme.basic 包的 `LayoutState` 中.
`LayoutStateService``dispatchAddNavigationElement` 方法添加元素到右侧的菜单.
你可以通过将模板添加到 `app.component` 调用 `dispatchAddNavigationElement` 方法来插入元素:
```js
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line
@Component({
selector: 'app-root',
template: `
<!-- Added below content -->
<ng-template #search
><input type="search" placeholder="Search" class="bg-transparent border-0"
/></ng-template>
`,
})
export class AppComponent {
// Added ViewChild
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>;
constructor(private layout: LayoutStateService) {} // injected LayoutStateService
// Added ngAfterViewInit
ngAfterViewInit() {
const newElement = {
name: 'Search',
element: this.searchElementRef,
order: 1,
} as Layout.NavigationElement;
this.layout.dispatchAddNavigationElement(newElement);
}
}
```
上面我们在菜单添加了一个搜索输入,最终UI如下:s
![navigation-menu-search-input](./images/navigation-menu-search-input.png)
## 如何删除右侧菜单元素
TODO
## 下一步是什么?
* [组件替换](./Component-Replacement.md)

BIN
docs/zh-Hans/UI/Angular/images/navigation-menu-after-patching.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/zh-Hans/UI/Angular/images/navigation-menu-via-app-routing.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/zh-Hans/UI/Angular/images/navigation-menu-via-config-state.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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

@ -333,6 +333,10 @@
"text": "配置状态",
"path": "UI/Angular/Config-State.md"
},
{
"text": "修改菜单",
"path": "UI/Angular/Modifying-the-Menu.md"
},
{
"text": "替换组件",
"path": "UI/Angular/Component-Replacement.md"

16
framework/Volo.Abp.sln

@ -277,7 +277,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.Identi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectExtending", "src\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj", "{D1815C77-16D6-4F99-8814-69065CD89FB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.ObjectExtending.Tests", "test\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj", "{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectExtending.Tests", "test\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj", "{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.TextTemplating", "src\Volo.Abp.TextTemplating\Volo.Abp.TextTemplating.csproj", "{9E53F91F-EACD-4191-A487-E727741F1311}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.TextTemplating.Tests", "test\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj", "{251C7FD3-D313-4BCE-8068-352EC7EEA275}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -829,6 +833,14 @@ Global
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.Build.0 = Release|Any CPU
{9E53F91F-EACD-4191-A487-E727741F1311}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E53F91F-EACD-4191-A487-E727741F1311}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E53F91F-EACD-4191-A487-E727741F1311}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E53F91F-EACD-4191-A487-E727741F1311}.Release|Any CPU.Build.0 = Release|Any CPU
{251C7FD3-D313-4BCE-8068-352EC7EEA275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{251C7FD3-D313-4BCE-8068-352EC7EEA275}.Debug|Any CPU.Build.0 = Debug|Any CPU
{251C7FD3-D313-4BCE-8068-352EC7EEA275}.Release|Any CPU.ActiveCfg = Release|Any CPU
{251C7FD3-D313-4BCE-8068-352EC7EEA275}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -970,6 +982,8 @@ Global
{E1963439-2BE5-4DB5-8438-2A9A792A1ADA} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{D1815C77-16D6-4F99-8814-69065CD89FB3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{9E53F91F-EACD-4191-A487-E727741F1311} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl.json → framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl-PL.json

@ -1,5 +1,5 @@
{
"culture": "pl",
"culture": "pl-PL",
"texts": {
"GivenTenantIsNotAvailable": "Podany tenant jest niedostępny: {0}",
"Tenant": "Tenant",

56
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs

@ -1,9 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using IdentityModel.Client;
using Polly;
using Polly.Extensions.Http;
using Volo.Abp.Cli.Auth;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Cli.Http
{
@ -41,5 +47,55 @@ namespace Volo.Abp.Cli.Http
client.SetBearerToken(accessToken);
}
}
public async Task<HttpResponseMessage> GetHttpResponseMessageWithRetryAsync<T>
(
string url,
CancellationToken? cancellationToken = null,
ILogger<T> logger = null,
IEnumerable<TimeSpan> sleepDurations = null
)
{
if (sleepDurations == null)
{
sleepDurations = new[]
{
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(7)
};
}
if (!cancellationToken.HasValue)
{
cancellationToken = CancellationToken.None;
}
return await HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => !msg.IsSuccessStatusCode)
.WaitAndRetryAsync(sleepDurations,
(responseMessage, timeSpan, retryCount, context) =>
{
if (responseMessage.Exception != null)
{
string httpErrorCode = responseMessage.Result == null ?
httpErrorCode = string.Empty :
"HTTP-" + (int)responseMessage.Result.StatusCode + ", ";
logger?.LogWarning(
$"{retryCount}. HTTP request attempt failed to {url} with an error: {httpErrorCode}{responseMessage.Exception.Message}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
else if (responseMessage.Result != null)
{
logger?.LogWarning(
$"{retryCount}. HTTP request attempt failed to {url} with an error: {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
})
.ExecuteAsync(async () => await this.GetAsync(url, cancellationToken.Value));
}
}
}

39
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/AbpIoApiKeyService.cs

@ -13,6 +13,7 @@ using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
using Volo.Abp.Threading;
namespace Volo.Abp.Cli.Licensing
{
@ -20,14 +21,21 @@ namespace Volo.Abp.Cli.Licensing
{
protected IJsonSerializer JsonSerializer { get; }
protected IRemoteServiceExceptionHandler RemoteServiceExceptionHandler { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
private readonly ILogger<AbpIoApiKeyService> _logger;
private DeveloperApiKeyResult _apiKeyResult = null;
public AbpIoApiKeyService(IJsonSerializer jsonSerializer, IRemoteServiceExceptionHandler remoteServiceExceptionHandler, ILogger<AbpIoApiKeyService> logger)
public AbpIoApiKeyService(
IJsonSerializer jsonSerializer,
ICancellationTokenProvider cancellationTokenProvider,
IRemoteServiceExceptionHandler remoteServiceExceptionHandler,
ILogger<AbpIoApiKeyService> logger)
{
JsonSerializer = jsonSerializer;
RemoteServiceExceptionHandler = remoteServiceExceptionHandler;
_logger = logger;
CancellationTokenProvider = cancellationTokenProvider;
}
public async Task<DeveloperApiKeyResult> GetApiKeyOrNullAsync(bool invalidateCache = false)
@ -51,31 +59,10 @@ namespace Volo.Abp.Cli.Licensing
using (var client = new CliHttpClient())
{
var response = await HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => !msg.IsSuccessStatusCode)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(7)
},
(responseMessage, timeSpan, retryCount, context) =>
{
if (responseMessage.Exception != null)
{
_logger.LogWarning(
$"{retryCount}. request attempt failed to {url} with an error: \"{responseMessage.Exception.Message}\". " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
else if (responseMessage.Result != null)
{
_logger.LogWarning(
$"{retryCount}. request attempt failed {url} with {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
})
.ExecuteAsync(async () => await client.GetAsync(url));
var response = await client.GetHttpResponseMessageWithRetryAsync(
url: url,
cancellationToken: CancellationTokenProvider.Token,
logger: _logger);
if (!response.IsSuccessStatusCode)
{

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

@ -1,16 +1,10 @@
using Newtonsoft.Json;
using NuGet.Versioning;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Polly;
using Polly.Extensions.Http;
using Volo.Abp.Cli.Auth;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.Licensing;
@ -29,6 +23,8 @@ namespace Volo.Abp.Cli.NuGet
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IRemoteServiceExceptionHandler RemoteServiceExceptionHandler { get; }
private readonly IApiKeyService _apiKeyService;
private List<string> _proPackageList;
private DeveloperApiKeyResult _apiKeyResult;
public NuGetService(
IJsonSerializer jsonSerializer,
@ -45,20 +41,20 @@ namespace Volo.Abp.Cli.NuGet
public async Task<SemanticVersion> GetLatestVersionOrNullAsync(string packageId, bool includePreviews = false, bool includeNightly = false)
{
List<string> proPackageList = null;
if (AuthService.IsLoggedIn())
{
proPackageList = await GetProPackageListAsync();
if (_proPackageList == null)
{
_proPackageList = await GetProPackageListAsync();
}
}
string url;
if (includeNightly)
{
url =
$"https://www.myget.org/F/abp-nightly/api/v3/flatcontainer/{packageId.ToLowerInvariant()}/index.json";
url = $"https://www.myget.org/F/abp-nightly/api/v3/flatcontainer/{packageId.ToLowerInvariant()}/index.json";
}
else if (proPackageList?.Contains(packageId) ?? false)
else if (_proPackageList?.Contains(packageId) ?? false)
{
url = await GetNuGetUrlForCommercialPackage(packageId);
}
@ -67,15 +63,13 @@ namespace Volo.Abp.Cli.NuGet
url = $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json";
}
using (var client = new CliHttpClient(setBearerToken: false))
{
var responseMessage = await GetHttpResponseMessageWithRetryAsync(client, url);
if (!responseMessage.IsSuccessStatusCode)
{
throw new Exception($"ERROR: Remote server returns '{responseMessage.StatusCode}'");
}
var responseMessage = await client.GetHttpResponseMessageWithRetryAsync(
url,
cancellationToken: CancellationTokenProvider.Token,
logger: Logger
);
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(responseMessage);
@ -98,62 +92,41 @@ namespace Volo.Abp.Cli.NuGet
private async Task<string> GetNuGetUrlForCommercialPackage(string packageId)
{
var apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync();
return CliUrls.GetNuGetPackageInfoUrl(apiKeyResult.ApiKey, packageId);
}
if (_apiKeyResult == null)
{
_apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync();
}
private async Task<HttpResponseMessage> GetHttpResponseMessageWithRetryAsync(HttpClient client, string url)
{
return await HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => !msg.IsSuccessStatusCode)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(7)
},
(responseMessage, timeSpan, retryCount, context) =>
{
if (responseMessage.Exception != null)
{
Logger.LogWarning(
$"{retryCount}. HTTP request attempt failed to {url} with an error: HTTP {(int)responseMessage.Result.StatusCode}-{responseMessage.Exception.Message}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
else if (responseMessage.Result != null)
{
Logger.LogWarning(
$"{retryCount}. HTTP request attempt failed to {url} with an error: {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
})
.ExecuteAsync(async () => await client.GetAsync(url, CancellationTokenProvider.Token));
return CliUrls.GetNuGetPackageInfoUrl(_apiKeyResult.ApiKey, packageId);
}
private async Task<List<string>> GetProPackageListAsync()
{
using var client = new CliHttpClient();
var responseMessage = await client.GetAsync(
$"{CliUrls.WwwAbpIo}api/app/nugetPackage/proPackageNames",
CancellationTokenProvider.Token
var url = $"{CliUrls.WwwAbpIo}api/app/nugetPackage/proPackageNames";
var responseMessage = await client.GetHttpResponseMessageWithRetryAsync(
url: url,
cancellationToken: CancellationTokenProvider.Token,
logger: Logger
);
if (!responseMessage.IsSuccessStatusCode)
if (responseMessage.IsSuccessStatusCode)
{
var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. ";
var remoteServiceErrorMessage = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage);
return JsonSerializer.Deserialize<List<string>>(await responseMessage.Content.ReadAsStringAsync());
}
if (remoteServiceErrorMessage != null)
{
exceptionMessage += remoteServiceErrorMessage;
}
Logger.LogInformation(exceptionMessage);
return null;
var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. ";
var remoteServiceErrorMessage = await RemoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage);
if (remoteServiceErrorMessage != null)
{
exceptionMessage += remoteServiceErrorMessage;
}
return JsonSerializer.Deserialize<List<string>>(await responseMessage.Content.ReadAsStringAsync());
Logger.LogError(exceptionMessage);
return null;
}
public class NuGetVersionResultDto

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs

@ -20,7 +20,7 @@ namespace Volo.Abp.Cli.ProjectModification
Logger = NullLogger<MyGetPackageListFinder>.Instance;
}
public async Task<MyGetApiResponse> GetPackages()
public async Task<MyGetApiResponse> GetPackagesAsync()
{
if (_response != null)
{

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

@ -13,25 +13,31 @@ using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.IO;
using Volo.Abp.Threading;
namespace Volo.Abp.Cli.ProjectModification
{
public class NpmPackagesUpdater : ITransientDependency
{
public ILogger<NpmPackagesUpdater> Logger { get; set; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
private readonly PackageJsonFileFinder _packageJsonFileFinder;
private readonly NpmGlobalPackagesChecker _npmGlobalPackagesChecker;
private readonly MyGetPackageListFinder _myGetPackageListFinder;
private readonly Dictionary<string, string> _fileVersionStorage = new Dictionary<string, string>();
private MyGetApiResponse _myGetApiResponse;
public NpmPackagesUpdater(PackageJsonFileFinder packageJsonFileFinder, NpmGlobalPackagesChecker npmGlobalPackagesChecker, MyGetPackageListFinder myGetPackageListFinder)
public NpmPackagesUpdater(
PackageJsonFileFinder packageJsonFileFinder,
NpmGlobalPackagesChecker npmGlobalPackagesChecker,
MyGetPackageListFinder myGetPackageListFinder,
ICancellationTokenProvider cancellationTokenProvider)
{
_packageJsonFileFinder = packageJsonFileFinder;
_npmGlobalPackagesChecker = npmGlobalPackagesChecker;
_myGetPackageListFinder = myGetPackageListFinder;
CancellationTokenProvider = cancellationTokenProvider;
Logger = NullLogger<NpmPackagesUpdater>.Instance;
}
@ -77,7 +83,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private async Task DeleteNpmrcFileAsync(string directoryName)
private static async Task DeleteNpmrcFileAsync(string directoryName)
{
FileHelper.DeleteIfExists(Path.Combine(directoryName, ".npmrc"));
@ -135,11 +141,13 @@ namespace Volo.Abp.Cli.ProjectModification
{
using (var client = new CliHttpClient(TimeSpan.FromMinutes(1)))
{
var responseMessage = await client.GetAsync(
$"{CliUrls.WwwAbpIo}api/myget/apikey/"
var response = await client.GetHttpResponseMessageWithRetryAsync(
url: $"{CliUrls.WwwAbpIo}api/myget/apikey/",
cancellationToken: CancellationTokenProvider.Token,
logger: Logger
);
return Encoding.Default.GetString(await responseMessage.Content.ReadAsByteArrayAsync());
return Encoding.Default.GetString(await response.Content.ReadAsByteArrayAsync());
}
}
catch (Exception)
@ -148,26 +156,26 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private bool IsAngularProject(string fileDirectory)
private static bool IsAngularProject(string fileDirectory)
{
return File.Exists(Path.Combine(fileDirectory, "angular.json"));
}
protected virtual async Task<bool> UpdatePackagesInFile(string file, bool includePreviews = false, bool switchToStable = false)
protected virtual async Task<bool> UpdatePackagesInFile(string filePath, bool includePreviews = false, bool switchToStable = false)
{
var packagesUpdated = false;
var fileContent = File.ReadAllText(file);
var fileContent = File.ReadAllText(filePath);
var packageJson = JObject.Parse(fileContent);
var abpPackages = GetAbpPackagesFromPackageJson(packageJson);
if (!abpPackages.Any())
{
return packagesUpdated;
return false;
}
foreach (var abpPackage in abpPackages)
{
var updated = await TryUpdatePackage(file, abpPackage, includePreviews, switchToStable);
var updated = await TryUpdatingPackage(filePath, abpPackage, includePreviews, switchToStable);
if (updated)
{
@ -175,15 +183,18 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
var modifiedFileContent = packageJson.ToString(Formatting.Indented);
var updatedContent = packageJson.ToString(Formatting.Indented);
File.WriteAllText(file, modifiedFileContent);
File.WriteAllText(filePath, updatedContent);
return packagesUpdated;
}
protected virtual async Task<bool> TryUpdatePackage(string file, JProperty package,
bool includePreviews = false, bool switchToStable = false)
protected virtual async Task<bool> TryUpdatingPackage(
string filePath,
JProperty package,
bool includePreviews = false,
bool switchToStable = false)
{
var currentVersion = (string)package.Value;
@ -198,23 +209,31 @@ namespace Volo.Abp.Cli.ProjectModification
package.Value.Replace(versionWithPrefix);
Logger.LogInformation($"Updated {package.Name} to {version} in {file.Replace(Directory.GetCurrentDirectory(), "")}.");
Logger.LogInformation($"Updated {package.Name} to {version} in {filePath.Replace(Directory.GetCurrentDirectory(), "")}.");
return true;
}
protected virtual async Task<string> GetLatestVersion(JProperty package, string currentVersion,
bool includePreviews = false, bool switchToStable = false)
protected virtual async Task<string> GetLatestVersion(
JProperty package,
string currentVersion,
bool includePreviews = false,
bool switchToStable = false)
{
if (_fileVersionStorage.ContainsKey(package.Name))
{
return _fileVersionStorage[package.Name];
}
string newVersion = currentVersion;
var newVersion = currentVersion;
if (includePreviews || (!switchToStable && currentVersion.Contains("-preview")))
{
var mygetPackage = (await _myGetPackageListFinder.GetPackages()).Packages.FirstOrDefault(p => p.Id == package.Name);
if (_myGetApiResponse == null)
{
_myGetApiResponse = await _myGetPackageListFinder.GetPackagesAsync();
}
var mygetPackage = _myGetApiResponse.Packages.FirstOrDefault(p => p.Id == package.Name);
if (mygetPackage != null)
{
newVersion = mygetPackage.Versions.Last();
@ -225,7 +244,6 @@ namespace Volo.Abp.Cli.ProjectModification
newVersion = CmdHelper.RunCmdAndGetOutput($"npm show {package.Name} version");
}
_fileVersionStorage[package.Name] = newVersion;
return newVersion;

27
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs

@ -42,8 +42,9 @@ namespace Volo.Abp.Cli.ProjectModification
protected virtual async Task UpdateInternalAsync(string projectPath, bool includePreviews = false, bool switchToStable = false)
{
var fileContent = File.ReadAllText(projectPath);
var updatedContent = await UpdateVoloPackagesAsync(fileContent, includePreviews, switchToStable);
File.WriteAllText(projectPath, await UpdateVoloPackagesAsync(fileContent, includePreviews, switchToStable));
File.WriteAllText(projectPath, updatedContent);
}
private async Task<string> UpdateVoloPackagesAsync(string content, bool includePreviews = false, bool switchToStable = false)
@ -74,16 +75,13 @@ namespace Volo.Abp.Cli.ProjectModification
var versionAttribute = package.Attributes["Version"];
var currentVersion = versionAttribute.Value;
var packageVersion = SemanticVersion.Parse(currentVersion);
Logger.LogDebug("Checking package: \"{0}\" - Current version: {1}", packageId, packageVersion);
var currentSemanticVersion = SemanticVersion.Parse(currentVersion);
Logger.LogDebug("Checking package: \"{0}\" - Current version: {1}", packageId, currentSemanticVersion);
if (includePreviews || (currentVersion.Contains("-preview") && !switchToStable))
{
var latestVersion = (await _myGetPackageListFinder.GetPackages()).Packages
.FirstOrDefault(p => p.Id == packageId)
?.Versions.LastOrDefault();
var latestVersion = await GetLatestVersionFromMyGet(packageId);
if (currentVersion != latestVersion)
{
@ -99,14 +97,14 @@ namespace Volo.Abp.Cli.ProjectModification
{
var latestVersion = await _nuGetService.GetLatestVersionOrNullAsync(packageId);
if (latestVersion != null && (currentVersion.Contains("-preview") || packageVersion < latestVersion))
if (latestVersion != null && (currentVersion.Contains("-preview") || currentSemanticVersion < latestVersion))
{
Logger.LogInformation("Updating package \"{0}\" from v{1} to v{2}.", packageId, packageVersion.ToString(), latestVersion.ToString());
Logger.LogInformation("Updating package \"{0}\" from v{1} to v{2}.", packageId, currentSemanticVersion.ToString(), latestVersion.ToString());
versionAttribute.Value = latestVersion.ToString();
}
else
{
Logger.LogInformation("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion);
Logger.LogInformation("Package: \"{0}-v{1}\" is up to date.", packageId, currentSemanticVersion);
}
}
}
@ -116,11 +114,18 @@ namespace Volo.Abp.Cli.ProjectModification
}
catch (Exception ex)
{
Logger.LogError("Cannot update volo packages! An error occured while updating the package \"{0}\". Error: {1}", packageId, ex.Message);
Logger.LogError("Cannot update Volo.* packages! An error occured while updating the package \"{0}\". Error: {1}", packageId, ex.Message);
Logger.LogException(ex);
}
return await Task.FromResult(content);
}
private async Task<string> GetLatestVersionFromMyGet(string packageId)
{
var myGetPack = await _myGetPackageListFinder.GetPackagesAsync();
return myGetPack.Packages.FirstOrDefault(p => p.Id == packageId)?.Versions.LastOrDefault();
}
}
}

14
framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs

@ -10,7 +10,12 @@ namespace Volo.Abp.Localization
{
Check.NotNull(culture, nameof(culture));
return Use(new CultureInfo(culture), uiCulture == null ? null : new CultureInfo(uiCulture));
return Use(
new CultureInfo(culture),
uiCulture == null
? null
: new CultureInfo(uiCulture)
);
}
public static IDisposable Use([NotNull] CultureInfo culture, CultureInfo uiCulture = null)
@ -29,5 +34,12 @@ namespace Volo.Abp.Localization
CultureInfo.CurrentUICulture = currentUiCulture;
});
}
public static string GetBaseCultureName(string cultureName)
{
return cultureName.Contains("-")
? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal))
: cultureName;
}
}
}

9
framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj

@ -14,24 +14,21 @@
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Volo\Abp\Emailing\Templates\DefaultEmailTemplates\*\*.tpl" />
<None Remove="Volo\Abp\Emailing\Templates\DefaultEmailTemplates\*\*.tpl" />
</ItemGroup>
<ItemGroup>
<Content Remove="Volo\Abp\Emailing\Localization\*.json" />
<EmbeddedResource Include="Volo\Abp\Emailing\Localization\*.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Scriban" Version="2.1.1" />
<Content Remove="Volo\Abp\Emailing\Templates\**\*.tpl" />
<EmbeddedResource Include="Volo\Abp\Emailing\Templates\**\*.tpl" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BackgroundJobs.Abstractions\Volo.Abp.BackgroundJobs.Abstractions.csproj" />
<ProjectReference Include="..\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
<ProjectReference Include="..\Volo.Abp.Settings\Volo.Abp.Settings.csproj" />
<ProjectReference Include="..\Volo.Abp.TextTemplating\Volo.Abp.TextTemplating.csproj" />
<ProjectReference Include="..\Volo.Abp.VirtualFileSystem\Volo.Abp.VirtualFileSystem.csproj" />
</ItemGroup>

51
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/AbpEmailingModule.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.Emailing.Localization;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Settings;
using Volo.Abp.TextTemplating;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Emailing
@ -15,15 +12,11 @@ namespace Volo.Abp.Emailing
typeof(AbpSettingsModule),
typeof(AbpVirtualFileSystemModule),
typeof(AbpBackgroundJobsAbstractionsModule),
typeof(AbpLocalizationModule)
typeof(AbpLocalizationModule),
typeof(AbpTextTemplatingModule)
)]
public class AbpEmailingModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
AutoAddDefinitionProviders(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
@ -43,41 +36,5 @@ namespace Volo.Abp.Emailing
options.AddJob<BackgroundEmailSendingJob>();
});
}
private static void AutoAddDefinitionProviders(IServiceCollection services)
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
{
if (typeof(IEmailTemplateDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{
definitionProviders.Add(context.ImplementationType);
}
});
services.Configure<AbpEmailTemplateOptions>(options =>
{
options.DefinitionProviders.AddIfNotContains(definitionProviders);
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
using (var scope = context.ServiceProvider.CreateScope())
{
var emailTemplateDefinitionManager =
scope.ServiceProvider.GetRequiredService<IEmailTemplateDefinitionManager>();
foreach (var templateDefinition in emailTemplateDefinitionManager.GetAll())
{
foreach (var contributor in templateDefinition.Contributors)
{
contributor.Initialize(new EmailTemplateInitializationContext(templateDefinition, scope.ServiceProvider));
}
}
}
}
}
}

18
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/AbpEmailTemplateOptions.cs

@ -1,18 +0,0 @@
using Volo.Abp.Collections;
namespace Volo.Abp.Emailing.Templates
{
public class AbpEmailTemplateOptions
{
public string DefaultLayout { get; set; }
public ITypeList<IEmailTemplateDefinitionProvider> DefinitionProviders { get; }
public AbpEmailTemplateOptions()
{
DefaultLayout = StandardEmailTemplates.DefaultLayout;
DefinitionProviders = new TypeList<IEmailTemplateDefinitionProvider>();
}
}
}

24
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplateProvider.cs

@ -1,16 +1,26 @@
using Volo.Abp.Emailing.Templates.VirtualFiles;
using Volo.Abp.TextTemplating;
namespace Volo.Abp.Emailing.Templates
{
public class DefaultEmailTemplateProvider : EmailTemplateDefinitionProvider
public class DefaultEmailTemplateProvider : TemplateDefinitionProvider
{
public override void Define(IEmailTemplateDefinitionContext context)
public override void Define(ITemplateDefinitionContext context)
{
context.Add(new EmailTemplateDefinition(StandardEmailTemplates.DefaultLayout, defaultCultureName: "en", isLayout: true, layout: null)
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout"));
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Layout,
defaultCultureName: "en",
isLayout: true
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Layout")
);
context.Add(new EmailTemplateDefinition(StandardEmailTemplates.SimpleMessage, defaultCultureName: "en")
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message"));
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Message,
defaultCultureName: "en",
layout: StandardEmailTemplates.Layout
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Message")
);
}
}
}

1
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message/en.tpl

@ -1 +0,0 @@
{{message}}

42
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplate.cs

@ -1,42 +0,0 @@
using System.Text;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplate
{
public EmailTemplateDefinition Definition { get; }
public string Content => ContentBuilder.ToString();
protected StringBuilder ContentBuilder { get; set; }
public EmailTemplate(string content, EmailTemplateDefinition definition)
{
ContentBuilder = new StringBuilder(content);
Definition = definition;
}
public virtual void SetLayout(EmailTemplate layoutTemplate)
{
if (!layoutTemplate.Definition.IsLayout)
{
throw new AbpException($"Given template is not a layout template: {layoutTemplate.Definition.Name}");
}
var newStrBuilder = new StringBuilder(layoutTemplate.Content);
newStrBuilder.Replace("{{#content}}", ContentBuilder.ToString());
ContentBuilder = newStrBuilder;
}
public virtual void SetContent(string content)
{
ContentBuilder = new StringBuilder(content);
}
public virtual void Replace(string name, string value)
{
ContentBuilder.Replace("{{" + name + "}}", value);
}
}
}

22
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateContributorList.cs

@ -1,22 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateContributorList : List<IEmailTemplateContributor>
{
public string GetOrNull(string cultureName)
{
foreach (var contributor in this.AsQueryable().Reverse())
{
var templateString = contributor.GetOrNull(cultureName);
if (templateString != null)
{
return templateString;
}
}
return null;
}
}
}

36
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinition.cs

@ -1,36 +0,0 @@
using System;
using JetBrains.Annotations;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinition
{
public const string DefaultLayoutPlaceHolder = "_";
public string Name { get; }
public bool IsLayout { get; }
public string Layout { get; set; }
public Type LocalizationResource { get; set; }
public EmailTemplateContributorList Contributors { get; }
public string DefaultCultureName { get; }
public bool SingleTemplateFile { get; }
public EmailTemplateDefinition([NotNull] string name, Type localizationResource = null, bool isLayout = false,
string layout = DefaultLayoutPlaceHolder, string defaultCultureName = null, bool singleTemplateFile = false)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
LocalizationResource = localizationResource;
Contributors = new EmailTemplateContributorList();
IsLayout = isLayout;
Layout = layout;
DefaultCultureName = defaultCultureName;
SingleTemplateFile = singleTemplateFile;
}
}
}

38
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionContext.cs

@ -1,38 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinitionContext : IEmailTemplateDefinitionContext
{
protected Dictionary<string, EmailTemplateDefinition> EmailTemplates { get; }
public EmailTemplateDefinitionContext(Dictionary<string, EmailTemplateDefinition> emailTemplates)
{
EmailTemplates = emailTemplates;
}
public virtual EmailTemplateDefinition GetOrNull(string name)
{
return EmailTemplates.GetOrDefault(name);
}
public virtual IReadOnlyList<EmailTemplateDefinition> GetAll()
{
return EmailTemplates.Values.ToImmutableList();
}
public virtual void Add(params EmailTemplateDefinition[] definitions)
{
if (definitions.IsNullOrEmpty())
{
return;
}
foreach (var definition in definitions)
{
EmailTemplates[definition.Name] = definition;
}
}
}
}

22
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionDictionary.cs

@ -1,22 +0,0 @@
using System.Collections.Generic;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinitionDictionary : Dictionary<string, EmailTemplateDefinition>
{
public EmailTemplateDefinitionDictionary Add(EmailTemplateDefinition emailTemplateDefinition)
{
if (ContainsKey(emailTemplateDefinition.Name))
{
throw new AbpException(
"There is already an email template definition with given name: " +
emailTemplateDefinition.Name
);
}
this[emailTemplateDefinition.Name] = emailTemplateDefinition;
return this;
}
}
}

74
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionManager.cs

@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateDefinitionManager : IEmailTemplateDefinitionManager, ISingletonDependency
{
protected Lazy<IDictionary<string, EmailTemplateDefinition>> EmailTemplateDefinitions { get; }
protected AbpEmailTemplateOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
public EmailTemplateDefinitionManager(
IOptions<AbpEmailTemplateOptions> options,
IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Options = options.Value;
EmailTemplateDefinitions =
new Lazy<IDictionary<string, EmailTemplateDefinition>>(CreateEmailTemplateDefinitions, true);
}
public virtual EmailTemplateDefinition Get(string name)
{
Check.NotNull(name, nameof(name));
var template = GetOrNull(name);
if (template == null)
{
throw new AbpException("Undefined template: " + name);
}
return template;
}
public virtual IReadOnlyList<EmailTemplateDefinition> GetAll()
{
return EmailTemplateDefinitions.Value.Values.ToImmutableList();
}
public virtual EmailTemplateDefinition GetOrNull(string name)
{
return EmailTemplateDefinitions.Value.GetOrDefault(name);
}
protected virtual IDictionary<string, EmailTemplateDefinition> CreateEmailTemplateDefinitions()
{
var templates = new Dictionary<string, EmailTemplateDefinition>();
using (var scope = ServiceProvider.CreateScope())
{
var providers = Options
.DefinitionProviders
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IEmailTemplateDefinitionProvider)
.ToList();
foreach (var provider in providers)
{
provider.Define(new EmailTemplateDefinitionContext(templates));
}
}
return templates;
}
}
}

9
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionProvider.cs

@ -1,9 +0,0 @@
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Emailing.Templates
{
public abstract class EmailTemplateDefinitionProvider : IEmailTemplateDefinitionProvider, ITransientDependency
{
public abstract void Define(IEmailTemplateDefinitionContext context);
}
}

18
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateInitializationContext.cs

@ -1,18 +0,0 @@
using System;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateInitializationContext
{
public EmailTemplateDefinition EmailTemplateDefinition { get; }
public IServiceProvider ServiceProvider { get; }
public EmailTemplateInitializationContext(EmailTemplateDefinition emailTemplateDefinition,
IServiceProvider serviceProvider)
{
EmailTemplateDefinition = emailTemplateDefinition;
ServiceProvider = serviceProvider;
}
}
}

121
framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateProvider.cs

@ -1,121 +0,0 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
namespace Volo.Abp.Emailing.Templates
{
public class EmailTemplateProvider : IEmailTemplateProvider, ITransientDependency
{
protected IEmailTemplateDefinitionManager EmailTemplateDefinitionManager;
protected ITemplateLocalizer TemplateLocalizer { get; }
protected AbpEmailTemplateOptions Options { get; }
protected IStringLocalizerFactory StringLocalizerFactory;
public EmailTemplateProvider(IEmailTemplateDefinitionManager emailTemplateDefinitionManager,
ITemplateLocalizer templateLocalizer, IStringLocalizerFactory stringLocalizerFactory,
IOptions<AbpEmailTemplateOptions> options)
{
EmailTemplateDefinitionManager = emailTemplateDefinitionManager;
TemplateLocalizer = templateLocalizer;
StringLocalizerFactory = stringLocalizerFactory;
Options = options.Value;
}
public async Task<EmailTemplate> GetAsync(string name)
{
return await GetAsync(name, CultureInfo.CurrentUICulture.Name);
}
public async Task<EmailTemplate> GetAsync(string name, string cultureName)
{
return await GetInternalAsync(name, cultureName);
}
protected virtual async Task<EmailTemplate> GetInternalAsync(string name, string cultureName)
{
var emailTemplateDefinition = EmailTemplateDefinitionManager.GetOrNull(name);
if (emailTemplateDefinition == null)
{
// TODO: Localized message
throw new AbpException($"email template {name} not definition");
}
var emailTemplateString = emailTemplateDefinition.Contributors.GetOrNull(cultureName);
if (emailTemplateString == null && emailTemplateDefinition.DefaultCultureName != null)
{
emailTemplateString =
emailTemplateDefinition.Contributors.GetOrNull(emailTemplateDefinition.DefaultCultureName);
if (emailTemplateString != null)
{
cultureName = emailTemplateDefinition.DefaultCultureName;
}
}
if (emailTemplateString != null)
{
var emailTemplate = new EmailTemplate(emailTemplateString, emailTemplateDefinition);
await SetLayoutAsync(emailTemplateDefinition, emailTemplate, cultureName);
if (emailTemplateDefinition.SingleTemplateFile)
{
await LocalizeAsync(emailTemplateDefinition, emailTemplate, cultureName);
}
return emailTemplate;
}
// TODO: Localized message
throw new AbpException($"{cultureName} template not exist!");
}
protected virtual async Task SetLayoutAsync(EmailTemplateDefinition emailTemplateDefinition,
EmailTemplate emailTemplate, string cultureName)
{
var layout = emailTemplateDefinition.Layout;
if (layout.IsNullOrWhiteSpace())
{
return;
}
if (layout == EmailTemplateDefinition.DefaultLayoutPlaceHolder)
{
layout = Options.DefaultLayout;
}
var layoutTemplate = await GetInternalAsync(layout, cultureName);
emailTemplate.SetLayout(layoutTemplate);
}
protected virtual Task LocalizeAsync(EmailTemplateDefinition emailTemplateDefinition,
EmailTemplate emailTemplate, string cultureName)
{
if (emailTemplateDefinition.LocalizationResource == null)
{
return Task.CompletedTask;
}
var localizer = StringLocalizerFactory.Create(emailTemplateDefinition.LocalizationResource);
if (cultureName != null)
{
using (CultureHelper.Use(new CultureInfo(cultureName)))
{
emailTemplate.SetContent(TemplateLocalizer.Localize(localizer, emailTemplate.Content));
}
}
else
{
emailTemplate.SetContent(
TemplateLocalizer.Localize(localizer, emailTemplate.Content)
);
}
return Task.CompletedTask;
}
}
}

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

Loading…
Cancel
Save