` 标签:
+
+```html
+
+
+
+
+
+ {%{{{ "::Actions" | abpLocalization }}}%}
+ {%{{{ "::Name" | abpLocalization }}}%}
+ {%{{{ "::Type" | abpLocalization }}}%}
+ {%{{{ "::PublishDate" | abpLocalization }}}%}
+ {%{{{ "::Price" | abpLocalization }}}%}
+
+
+
+
+
+
+
+ {%{{{ "::Actions" | abpLocalization }}}%}
+
+
+
+ {%{{{ "::Edit" | abpLocalization }}}%}
+
+
+
+
+ {%{{{ data.name }}}%}
+ {%{{{ booksType[data.type] }}}%}
+ {%{{{ data.publishDate | date }}}%}
+ {%{{{ data.price }}}%}
+
+
+
+```
+
+- 我们添加了 "Actions" 栏的 `th`.
+- 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作.
+- 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单.
+
+UI最终看起来像这样:
+
+
+
+打开 `app\books\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
` 标签:
+
+```html
+
+ {%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}
+
+```
+
+* **Edit** 文本做为编辑记录操作的标题, **New Book** 做为添加记录操作的标题.
+
+### 删除图书
+
+#### DeleteBook 动作
+
+打开 `books\state` 文件夹下的 `books.actions.ts` 文件添加名为 `DeleteBook` 的动作.
+
+```js
+export class DeleteBook {
+ static readonly type = '[Books] Delete';
+ constructor(public id: string) {}
+}
+```
+
+打开 `books\state` 文件夹下的 `books.state.ts` 文件,使用以下内容替换它:
+
+```js
+import { PagedResultDto } from '@abp/ng.core';
+import { State, Action, StateContext, Selector } from '@ngxs/store';
+import { GetBooks, CreateUpdateBook, DeleteBook } from './books.actions'; // <== added DeleteBook==>
+import { BookService } from '../../app/shared/services';
+import { tap } from 'rxjs/operators';
+import { Injectable } from '@angular/core';
+import { BookDto } from '../../app/shared/models';
+
+export class BooksStateModel {
+ public book: PagedResultDto;
+}
+
+@State({
+ name: 'BooksState',
+ defaults: { book: {} } as BooksStateModel,
+})
+@Injectable()
+export class BooksState {
+ @Selector()
+ static getBooks(state: BooksStateModel) {
+ return state.book.items || [];
+ }
+
+ constructor(private bookService: BookService) {}
+
+ @Action(GetBooks)
+ get(ctx: StateContext) {
+ return this.bookService.getListByInput().pipe(
+ tap((booksResponse) => {
+ ctx.patchState({
+ book: booksResponse,
+ });
+ })
+ );
+ }
+
+ @Action(CreateUpdateBook)
+ save(ctx: StateContext, action: CreateUpdateBook) {
+ if (action.id) {
+ return this.bookService.updateByIdAndInput(action.payload, action.id);
+ } else {
+ return this.bookService.createByInput(action.payload);
+ }
+ }
+
+ // <== added DeleteBook action listener ==>
+ @Action(DeleteBook)
+ delete(ctx: StateContext, action: DeleteBook) {
+ return this.bookService.deleteById(action.id);
+ }
+}
+```
+
+- 我们导入了 `DeleteBook` .
+
+- 我们在文件末尾添加了 `DeleteBook` 动作监听器.
+
+#### 删除确认弹层
+
+打开 `app\books\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`.
+
+替换构造函数:
+
+```js
+import { ConfirmationService } from '@abp/ng.theme.shared';
+//...
+
+constructor(
+ private store: Store,
+ private fb: FormBuilder,
+ private bookService: BookService,
+ private confirmation: ConfirmationService // <== added this line ==>
+) { }
+```
+
+* 我们导入了 `ConfirmationService`.
+* 我们在构造函数注入了 `ConfirmationService` .
+
+参阅[确认弹层文档](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service)了解更多
+
+在 `book-list.component.ts` 中添加删除方法:
+
+```js
+import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==>
+
+import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==>
+
+//...
+
+delete(id: string) {
+ this.confirmation
+ .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
+ .subscribe(status => {
+ if (status === Confirmation.Status.confirm) {
+ this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
+ }
+ });
+}
+```
+
+`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时分派动作. 确认弹层看起来如下:
+
+
+
+#### 添加删除按钮
+
+打开 `app\books\book-list` 文件夹下的 `app\books\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮:
+
+```html
+
+
+
+ {%{{{ 'AbpAccount::Delete' | abpLocalization }}}%}
+
+
+```
+
+最终操作下拉框UI看起来如下:
+
+
+
+{{end}}
+
+### 下一章
+
+查看本教程的 [下一章](Part-3.md) .
diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md
new file mode 100644
index 0000000000..cded954e6b
--- /dev/null
+++ b/docs/zh-Hans/Tutorials/Part-3.md
@@ -0,0 +1,198 @@
+## 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).
+
+### 解决方案中的测试项目
+
+解决方案中有多个测试项目:
+
+
+
+每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试:
+
+* [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 _bookRepository;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public BookStoreTestDataSeedContributor(
+ IRepository 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`并在`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();
+ }
+
+ [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(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 -> 测试资源管理器)并**执行**所有测试:
+
+
+
+恭喜, 绿色图标表示测试已成功通过!
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png
new file mode 100644
index 0000000000..e8243fedc7
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-actions-buttons.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png
rename to docs/zh-Hans/Tutorials/images/bookstore-add-create-dialog-v2.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png
rename to docs/zh-Hans/Tutorials/images/bookstore-add-edit-dialog.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png
rename to docs/zh-Hans/Tutorials/images/bookstore-add-index-page-v2.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png
new file mode 100644
index 0000000000..a3197b6457
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-angular-file-tree.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png
rename to docs/zh-Hans/Tutorials/images/bookstore-appservice-tests.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png
new file mode 100644
index 0000000000..a460d4241b
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-list-2.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png
new file mode 100644
index 0000000000..9e6cc9e010
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png
rename to docs/zh-Hans/Tutorials/images/bookstore-books-table-actions.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/zh-Hans/Tutorials/images/bookstore-books-table.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png
rename to docs/zh-Hans/Tutorials/images/bookstore-books-table.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png
new file mode 100644
index 0000000000..a80b180f1c
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-confirmation-popup.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png
rename to docs/zh-Hans/Tutorials/images/bookstore-create-dialog-2.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png
rename to docs/zh-Hans/Tutorials/images/bookstore-create-dialog.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png
new file mode 100644
index 0000000000..b9eb38b8b7
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-create-project-angular.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png
new file mode 100644
index 0000000000..f453b20279
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-create-project-mvc.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png
new file mode 100644
index 0000000000..6f19dcc7bf
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-book-list-terminal.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png
new file mode 100644
index 0000000000..ec9ef4c42f
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-creating-books-module-terminal.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png
new file mode 100644
index 0000000000..857b10de5b
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-ef.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png
new file mode 100644
index 0000000000..8d78bd9a54
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-database-tables-mongodb.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png
new file mode 100644
index 0000000000..bfc1c64797
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-edit-button.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png
new file mode 100644
index 0000000000..2a02802bb9
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-empty-new-book-modal.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png
new file mode 100644
index 0000000000..4f41829f0d
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-final-actions-dropdown.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png b/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png
new file mode 100644
index 0000000000..be7a919017
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-generate-state-books.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/zh-Hans/Tutorials/images/bookstore-homepage.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png
rename to docs/zh-Hans/Tutorials/images/bookstore-homepage.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png
rename to docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v2.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png
new file mode 100644
index 0000000000..591cffb121
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-initial-book-list-page.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png
new file mode 100644
index 0000000000..629ad46444
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-initial-books-page-with-layout.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png
new file mode 100644
index 0000000000..542cda209c
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-localization-files-v2.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/zh-Hans/Tutorials/images/bookstore-menu-items.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png
rename to docs/zh-Hans/Tutorials/images/bookstore-menu-items.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png
new file mode 100644
index 0000000000..0724e4ae8f
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-angular.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png
new file mode 100644
index 0000000000..d59c0ce1d3
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-migrations-applied-mvc.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png
new file mode 100644
index 0000000000..8112fe1352
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png
new file mode 100644
index 0000000000..72513de6e5
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form-v2.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png
new file mode 100644
index 0000000000..95de64f8d4
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-form.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png b/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png
new file mode 100644
index 0000000000..97bf7fc7c1
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-menu-item.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png b/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png
new file mode 100644
index 0000000000..a640eb2681
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-open-package-manager-console.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png
new file mode 100644
index 0000000000..2baea20236
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration-v2.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png
rename to docs/zh-Hans/Tutorials/images/bookstore-pmc-add-book-migration.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png b/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png
new file mode 100644
index 0000000000..cf6145e03f
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-service-terminal-output.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png
new file mode 100644
index 0000000000..07d064a880
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-angular.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png
rename to docs/zh-Hans/Tutorials/images/bookstore-solution-structure-mvc.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png
new file mode 100644
index 0000000000..08abf845a8
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-start-project-angular.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png b/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png
new file mode 100644
index 0000000000..133dc6f131
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-start-project-mvc.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png
new file mode 100644
index 0000000000..66d630bb56
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-swagger-book-dto-properties.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-swagger.png b/docs/zh-Hans/Tutorials/images/bookstore-swagger.png
new file mode 100644
index 0000000000..3ce36a11bc
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-swagger.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png
rename to docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist-network.png
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png
rename to docs/zh-Hans/Tutorials/images/bookstore-test-js-proxy-getlist.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png
new file mode 100644
index 0000000000..6a8947238e
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-angular.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png
rename to docs/zh-Hans/Tutorials/images/bookstore-test-projects-mvc.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png
new file mode 100644
index 0000000000..8701164d75
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-test-projects-v2.png differ
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png
new file mode 100644
index 0000000000..4889f4f757
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-update-database-after-book-entity.png differ
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/zh-Hans/Tutorials/images/bookstore-user-management.png
similarity index 100%
rename from docs/zh-Hans/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png
rename to docs/zh-Hans/Tutorials/images/bookstore-user-management.png
diff --git a/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png
new file mode 100644
index 0000000000..307e3516a5
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-visual-studio-solution-v3.png differ
diff --git a/docs/zh-Hans/Tutorials/images/generate-proxy-command.png b/docs/zh-Hans/Tutorials/images/generate-proxy-command.png
new file mode 100644
index 0000000000..f850ce07a2
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/generate-proxy-command.png differ
diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies.png b/docs/zh-Hans/Tutorials/images/generated-proxies.png
new file mode 100644
index 0000000000..9e466e7d55
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/generated-proxies.png differ
diff --git a/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png
new file mode 100644
index 0000000000..c9e2fc0e65
Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/mozilla-self-signed-cert-error.png differ
diff --git a/docs/zh-Hans/UI/Angular/Config-State.md b/docs/zh-Hans/UI/Angular/Config-State.md
index 4967dd78b3..de645c8968 100644
--- a/docs/zh-Hans/UI/Angular/Config-State.md
+++ b/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)
\ No newline at end of file
+- [修改菜单](./Modifying-the-Menu.md)
\ No newline at end of file
diff --git a/docs/zh-Hans/UI/Angular/Confirmation-Service.md b/docs/zh-Hans/UI/Angular/Confirmation-Service.md
index d6ff4bc3c5..a1a181837c 100644
--- a/docs/zh-Hans/UI/Angular/Confirmation-Service.md
+++ b/docs/zh-Hans/UI/Angular/Confirmation-Service.md
@@ -86,10 +86,32 @@ this.confirmation.warn(
- `messageLocalizationParams`是用于消息本地化的插值参数.
- `titleLocalizationParams` 是标题本地化的插值参数.
-With the options above, the confirmation popup looks like this:
+使用以上选项确认弹层窗口如下所示:

+你可以传递HTML字符串作为标题,消息或按钮文本. 例如:
+
+```js
+const options: Partial = {
+ yesText: ' Yes, delete it',
+};
+
+this.confirmation.warn(
+ `
+ Role Demo will be deleted
+
+ Do you confirm that?
+ `,
+ 'Are you sure? ',
+ options,
+);
+```
+
+由于这些值现在是HTML,因此应该手动处理本地化. 参阅[LocalizationService](./Localization#using-the-localization-service)了解如何实现.
+
+> 注意,Angular会清除所有字符串,并且并非每个HTML字符串都可以使用. 仅显示被Angular视为"安全"的值.
+
### 如何删除一个确认弹层
打开的确认弹出窗口可以通过 `clear` 方法手动删除:
diff --git a/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md
new file mode 100644
index 0000000000..ec3297ea78
--- /dev/null
+++ b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md
@@ -0,0 +1,197 @@
+# 修改菜单
+
+
+菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局.
+
+
+
+## 如何添加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属性后,导航菜单如下图所示:
+
+
+
+## 通过 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;
+
+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;
+
+this.config.dispatchAddRoute(newRoute);
+// returns a state stream which emits after dispatch action is complete
+```
+
+`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`.
+
+新路由看起来像这样:
+
+
+
+## 如何修改一个导航元素
+
+`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置.
+
+```js
+// this.config is instance of ConfigStateService
+// eIdentityRouteNames enum can be imported from @abp/ng.identity
+
+const newRouteConfig: Partial = {
+ 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_ 的子路由.
+
+修改后,导航元素看起来像这样:
+
+
+
+## 如何在菜单的右侧添加元素
+
+右侧的元素存储在 @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: `
+
+
+
+ `,
+})
+export class AppComponent {
+ // Added ViewChild
+ @ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef;
+
+ 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
+
+
+
+## 如何删除右侧菜单元素
+
+TODO
+
+## 下一步是什么?
+
+* [组件替换](./Component-Replacement.md)
diff --git a/docs/zh-Hans/UI/Angular/images/navigation-menu-after-patching.png b/docs/zh-Hans/UI/Angular/images/navigation-menu-after-patching.png
new file mode 100644
index 0000000000..2f6bf08c88
Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/navigation-menu-after-patching.png differ
diff --git a/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png b/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png
new file mode 100644
index 0000000000..ebdc05e3e0
Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png differ
diff --git a/docs/zh-Hans/UI/Angular/images/navigation-menu-via-app-routing.png b/docs/zh-Hans/UI/Angular/images/navigation-menu-via-app-routing.png
new file mode 100644
index 0000000000..4d5f61301c
Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/navigation-menu-via-app-routing.png differ
diff --git a/docs/zh-Hans/UI/Angular/images/navigation-menu-via-config-state.png b/docs/zh-Hans/UI/Angular/images/navigation-menu-via-config-state.png
new file mode 100644
index 0000000000..19944f154d
Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/navigation-menu-via-config-state.png differ
diff --git a/docs/zh-Hans/UI/Common/Utils/Linked-List.md b/docs/zh-Hans/UI/Common/Utils/Linked-List.md
index 871db960ab..53c356628f 100644
--- a/docs/zh-Hans/UI/Common/Utils/Linked-List.md
+++ b/docs/zh-Hans/UI/Common/Utils/Linked-List.md
@@ -1229,7 +1229,7 @@ find(predicate: ListIteratorFunction): ListNode | undefined
从链表中找到与给定谓词匹配的第一个节点:
```js
-list.addTailMany(['a', 'b', 'b', 'c']);
+list.addManyTail(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@@ -1251,7 +1251,7 @@ findIndex(predicate: ListIteratorFunction): number
从链表中找到与给定谓词匹配的第一个节点的位置:
```js
-list.addTailMany(['a', 'b', 'b', 'c']);
+list.addManyTail(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@@ -1279,7 +1279,7 @@ get(position: number): ListNode | undefined
查找并返回链表中特定位置的节点:
```js
-list.addTailMany(['a', 'b', 'c']);
+list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@@ -1303,7 +1303,7 @@ indexOf(value: T, compareFn?: ListComparisonFn): number
在链表中找到匹配给定值的第一个节点位置:
```js
-list.addTailMany(['a', 'b', 'b', 'c']);
+list.addManyTail(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@@ -1325,7 +1325,7 @@ i3 === -1
你可以自定义比较器
```js
-list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]);
+list.addManyTail([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@@ -1365,7 +1365,7 @@ forEach(iteratorFn: ListIteratorFn): void
从头到尾在链表中的所有节点上运行回调函数:
```js
-list.addTailMany(['a', 'b', 'c']);
+list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@@ -1381,7 +1381,7 @@ list.forEach((node, index) => console.log(node.value + index));
链表是可迭代的. 换句话说你可以使用诸如`for ... of`之类的方法.
```js
-list.addTailMany(['a', 'b', 'c']);
+list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@@ -1405,7 +1405,7 @@ toArray(): T[]
转换链表值为数组:
```js
-list.addTailMany(['a', 'b', 'c']);
+list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@@ -1427,7 +1427,7 @@ toNodeArray(): T[]
转换链表节点为数组:
```js
-list.addTailMany(['a', 'b', 'c']);
+list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@@ -1449,7 +1449,7 @@ toString(mapperFn: ListMapperFn = JSON.stringify): string
将链表转换为节点及其关系的字符串表示形式:
```js
-list.addTailMany(['a', 2, 'c', { k: 4, v: 'd' }]);
+list.addManyTail(['a', 2, 'c', { k: 4, v: 'd' }]);
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
@@ -1507,7 +1507,7 @@ export class ListNode {
- `previous`引用列表中的上一个节点.
```js
-list.addTailMany([ 0, 1, 2 ]);
+list.addManyTail([ 0, 1, 2 ]);
console.log(
list.head.value, // 0
diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json
index 240c42dc99..c70284637d 100644
--- a/docs/zh-Hans/docs-nav.json
+++ b/docs/zh-Hans/docs-nav.json
@@ -5,13 +5,7 @@
"items": [
{
"text": "从启动模板开始",
- "path": "Getting-Started-With-Startup-Templates.md",
- "items": [
- {
- "text": "ASP.NET Core MVC 模板",
- "path": "Getting-Started-AspNetCore-MVC-Template.md"
- }
- ]
+ "path": "Getting-Started.md"
},
{
"text": "从空项目开始",
@@ -339,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"
diff --git a/docs/zh-Hans/docs-params.json b/docs/zh-Hans/docs-params.json
new file mode 100644
index 0000000000..23d079f9bb
--- /dev/null
+++ b/docs/zh-Hans/docs-params.json
@@ -0,0 +1,28 @@
+{
+ "parameters": [
+ {
+ "name": "UI",
+ "displayName": "UI",
+ "values": {
+ "MVC": "MVC / Razor Pages",
+ "NG": "Angular"
+ }
+ },
+ {
+ "name": "DB",
+ "displayName": "Database",
+ "values": {
+ "EF": "Entity Framework Core",
+ "Mongo": "MongoDB"
+ }
+ },
+ {
+ "name": "Tiered",
+ "displayName": "Tiered",
+ "values": {
+ "No": "Not Tiered",
+ "Yes": "Tiered"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 3db0998855..5d6f3e99a2 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -277,7 +277,13 @@ 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating", "src\Volo.Abp.TextTemplating\Volo.Abp.TextTemplating.csproj", "{9E53F91F-EACD-4191-A487-E727741F1311}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating.Tests", "test\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj", "{251C7FD3-D313-4BCE-8068-352EC7EEA275}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -829,6 +835,18 @@ 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
+ {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -970,6 +988,9 @@ 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}
+ {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
index cc62a1e1b9..da99a3e948 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
@@ -1,4 +1,5 @@
using System;
+using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending;
using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
@@ -19,5 +20,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public MultiTenancyInfoDto MultiTenancy { get; set; }
public CurrentTenantDto CurrentTenant { get; set; }
+
+ public ObjectExtensionsDto ObjectExtensions { get; set; }
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs
index a5e6574e93..8064ff99d2 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs
@@ -14,6 +14,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public CurrentCultureDto CurrentCulture { get; set; }
+ public string DefaultResourceName { get; set; }
+
public ApplicationLocalizationConfigurationDto()
{
Values = new Dictionary>();
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs
new file mode 100644
index 0000000000..f87fef31aa
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs
@@ -0,0 +1,178 @@
+using System.Collections.Generic;
+using System.Linq;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Localization;
+using Volo.Abp.ObjectExtending;
+using Volo.Abp.ObjectExtending.Modularity;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ public class CachedObjectExtensionsDtoService : ICachedObjectExtensionsDtoService, ISingletonDependency
+ {
+ private volatile ObjectExtensionsDto _cachedValue;
+ private readonly object _syncLock = new object();
+
+ public virtual ObjectExtensionsDto Get()
+ {
+ if (_cachedValue == null)
+ {
+ lock (_syncLock)
+ {
+ if (_cachedValue == null)
+ {
+ _cachedValue = GenerateCacheValue();
+ }
+ }
+ }
+
+ return _cachedValue;
+ }
+
+ protected virtual ObjectExtensionsDto GenerateCacheValue()
+ {
+ var objectExtensionsDto = new ObjectExtensionsDto
+ {
+ Modules = new Dictionary()
+ };
+
+ foreach (var moduleConfig in ObjectExtensionManager.Instance.Modules())
+ {
+ objectExtensionsDto.Modules[moduleConfig.Key] = CreateModuleExtensionDto(moduleConfig.Value);
+ }
+
+ return objectExtensionsDto;
+ }
+
+ protected virtual ModuleExtensionDto CreateModuleExtensionDto(
+ ModuleExtensionConfiguration moduleConfig)
+ {
+ var moduleExtensionDto = new ModuleExtensionDto
+ {
+ Entities = new Dictionary()
+ };
+
+ foreach (var objectConfig in moduleConfig.Entities)
+ {
+ moduleExtensionDto.Entities[objectConfig.Key] = GetEntityExtensionDto(objectConfig.Value);
+ }
+
+ foreach (var customConfig in moduleConfig.Configuration.Where(c => !c.Key.StartsWith("_")))
+ {
+ moduleExtensionDto.Configuration[customConfig.Key] = customConfig.Value;
+ }
+
+ return moduleExtensionDto;
+ }
+
+ protected virtual EntityExtensionDto GetEntityExtensionDto(
+ EntityExtensionConfiguration entityConfig)
+ {
+ var entityExtensionDto = new EntityExtensionDto
+ {
+ Properties = new Dictionary(),
+ Configuration = new Dictionary()
+ };
+
+ foreach (var propertyConfig in entityConfig.GetProperties())
+ {
+ if (!propertyConfig.IsAvailableToClients)
+ {
+ continue;
+ }
+
+ entityExtensionDto.Properties[propertyConfig.Name] = CreateExtensionPropertyDto(propertyConfig);
+ }
+
+ foreach (var customConfig in entityConfig.Configuration.Where(c => !c.Key.StartsWith("_")))
+ {
+ entityExtensionDto.Configuration[customConfig.Key] = customConfig.Value;
+ }
+
+ return entityExtensionDto;
+ }
+
+ protected virtual ExtensionPropertyDto CreateExtensionPropertyDto(
+ ExtensionPropertyConfiguration propertyConfig)
+ {
+ var extensionPropertyDto = new ExtensionPropertyDto
+ {
+ Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(propertyConfig.Type),
+ TypeSimple = TypeHelper.GetSimplifiedName(propertyConfig.Type),
+ Attributes = new List(),
+ DisplayName = CreateDisplayNameDto(propertyConfig),
+ Configuration = new Dictionary(),
+ Api = new ExtensionPropertyApiDto
+ {
+ OnGet = new ExtensionPropertyApiGetDto
+ {
+ IsAvailable = propertyConfig.Api.OnGet.IsAvailable
+ },
+ OnCreate = new ExtensionPropertyApiCreateDto
+ {
+ IsAvailable = propertyConfig.Api.OnCreate.IsAvailable
+ },
+ OnUpdate = new ExtensionPropertyApiUpdateDto
+ {
+ IsAvailable = propertyConfig.Api.OnUpdate.IsAvailable
+ }
+ },
+ Ui = new ExtensionPropertyUiDto
+ {
+ OnCreateForm = new ExtensionPropertyUiFormDto
+ {
+ IsVisible = propertyConfig.UI.OnCreateForm.IsVisible
+ },
+ OnEditForm = new ExtensionPropertyUiFormDto
+ {
+ IsVisible = propertyConfig.UI.OnEditForm.IsVisible
+ },
+ OnTable = new ExtensionPropertyUiTableDto
+ {
+ IsVisible = propertyConfig.UI.OnTable.IsVisible
+ }
+ }
+ };
+
+ foreach (var attribute in propertyConfig.Attributes)
+ {
+ extensionPropertyDto.Attributes.Add(
+ ExtensionPropertyAttributeDto.Create(attribute)
+ );
+ }
+
+ foreach (var customConfig in propertyConfig.Configuration.Where(c => !c.Key.StartsWith("_")))
+ {
+ extensionPropertyDto.Configuration[customConfig.Key] = customConfig.Value;
+ }
+
+ return extensionPropertyDto;
+ }
+
+ protected virtual LocalizableStringDto CreateDisplayNameDto(ExtensionPropertyConfiguration propertyConfig)
+ {
+ if (propertyConfig.DisplayName == null)
+ {
+ return null;
+ }
+
+ if (propertyConfig.DisplayName is LocalizableString localizableStringInstance)
+ {
+ return new LocalizableStringDto(
+ localizableStringInstance.Name,
+ localizableStringInstance.ResourceType != null
+ ? LocalizationResourceNameAttribute.GetName(localizableStringInstance.ResourceType)
+ : null
+ );
+ }
+
+ if (propertyConfig.DisplayName is FixedLocalizableString fixedLocalizableString)
+ {
+ // "_" means don't use the default resource, but directly use the name.
+ return new LocalizableStringDto(fixedLocalizableString.Value, "_");
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs
new file mode 100644
index 0000000000..83093e0370
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/EntityExtensionDto.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class EntityExtensionDto
+ {
+ public Dictionary Properties { get; set; }
+
+ public Dictionary Configuration { get; set; }
+
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiCreateDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiCreateDto.cs
new file mode 100644
index 0000000000..a2a588a358
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiCreateDto.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyApiCreateDto
+ {
+ ///
+ /// Default: true.
+ ///
+ public bool IsAvailable { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiDto.cs
new file mode 100644
index 0000000000..a9b4ec5b24
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiDto.cs
@@ -0,0 +1,23 @@
+using JetBrains.Annotations;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ public class ExtensionPropertyApiDto
+ {
+ [NotNull]
+ public ExtensionPropertyApiGetDto OnGet { get; set; }
+
+ [NotNull]
+ public ExtensionPropertyApiCreateDto OnCreate { get; set; }
+
+ [NotNull]
+ public ExtensionPropertyApiUpdateDto OnUpdate { get; set; }
+
+ public ExtensionPropertyApiDto()
+ {
+ OnGet = new ExtensionPropertyApiGetDto();
+ OnCreate = new ExtensionPropertyApiCreateDto();
+ OnUpdate = new ExtensionPropertyApiUpdateDto();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiGetDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiGetDto.cs
new file mode 100644
index 0000000000..e50a45d15a
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiGetDto.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyApiGetDto
+ {
+ ///
+ /// Default: true.
+ ///
+ public bool IsAvailable { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiUpdateDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiUpdateDto.cs
new file mode 100644
index 0000000000..a517b258da
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyApiUpdateDto.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyApiUpdateDto
+ {
+ ///
+ /// Default: true.
+ ///
+ public bool IsAvailable { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs
new file mode 100644
index 0000000000..c1c19350c2
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyAttributeDto.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyAttributeDto
+ {
+ public string Type { get; set; }
+ public string TypeSimple { get; set; }
+ public Dictionary Configuration { get; set; }
+
+ public static ExtensionPropertyAttributeDto Create(Attribute attribute)
+ {
+ var attributeType = attribute.GetType();
+ var dto = new ExtensionPropertyAttributeDto
+ {
+ Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(attributeType),
+ TypeSimple = TypeHelper.GetSimplifiedName(attributeType),
+ Configuration = new Dictionary()
+ };
+
+ if (attribute is StringLengthAttribute stringLengthAttribute)
+ {
+ dto.Configuration["MaximumLength"] = stringLengthAttribute.MaximumLength;
+ dto.Configuration["MinimumLength"] = stringLengthAttribute.MinimumLength;
+ }
+
+ //TODO: Others!
+
+ return dto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs
new file mode 100644
index 0000000000..74214914b9
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyDto.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyDto
+ {
+ public string Type { get; set; }
+
+ public string TypeSimple { get; set; }
+
+ [CanBeNull]
+ public LocalizableStringDto DisplayName { get; set; }
+
+ public ExtensionPropertyApiDto Api { get; set; }
+
+ public ExtensionPropertyUiDto Ui { get; set; }
+
+ public List Attributes { get; set; }
+
+ public Dictionary Configuration { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiDto.cs
new file mode 100644
index 0000000000..2fb841ec5a
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiDto.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyUiDto
+ {
+ public ExtensionPropertyUiTableDto OnTable { get; set; }
+ public ExtensionPropertyUiFormDto OnCreateForm { get; set; }
+ public ExtensionPropertyUiFormDto OnEditForm { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiFormDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiFormDto.cs
new file mode 100644
index 0000000000..c4f6f5211c
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiFormDto.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyUiFormDto
+ {
+ public bool IsVisible { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiTableDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiTableDto.cs
new file mode 100644
index 0000000000..4c9e4a7ce0
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ExtensionPropertyUiTableDto.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ExtensionPropertyUiTableDto
+ {
+ public bool IsVisible { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs
new file mode 100644
index 0000000000..f0163c8b55
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ICachedObjectExtensionsDtoService.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ public interface ICachedObjectExtensionsDtoService
+ {
+ ObjectExtensionsDto Get();
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/LocalizableStringDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/LocalizableStringDto.cs
new file mode 100644
index 0000000000..b80352d791
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/LocalizableStringDto.cs
@@ -0,0 +1,21 @@
+using System;
+using JetBrains.Annotations;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class LocalizableStringDto
+ {
+ [NotNull]
+ public string Name { get; private set; }
+
+ [CanBeNull]
+ public string Resource { get; set; }
+
+ public LocalizableStringDto([NotNull] string name, string resource = null)
+ {
+ Name = Check.NotNullOrEmpty(name, nameof(name));
+ Resource = resource;
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionConfigurationDto.cs
new file mode 100644
index 0000000000..e077596f59
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionConfigurationDto.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ModuleExtensionDto
+ {
+ public Dictionary Entities { get; set; }
+
+ public Dictionary Configuration { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs
new file mode 100644
index 0000000000..fd9665eab1
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/ObjectExtensionsDto.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending
+{
+ [Serializable]
+ public class ObjectExtensionsDto
+ {
+ public Dictionary Modules { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
index 6f8ccf285a..4504906566 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
@@ -43,6 +43,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
await ProcessFieldsAsync(context, output);
+ RemoveFormGroupItemsNotInModel(context, output, list);
+
SetContent(context, output, list, childContent);
SetFormAttributes(context, output);
@@ -142,6 +144,13 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
}
}
}
+
+ protected virtual void RemoveFormGroupItemsNotInModel(TagHelperContext context, TagHelperOutput output, List items)
+ {
+ var models = GetModels(context, output);
+
+ items.RemoveAll(x => models.All(m => !m.Name.Equals(x.PropertyName, StringComparison.InvariantCultureIgnoreCase)));
+ }
protected virtual async Task ProcessSelectGroupAsync(TagHelperContext context, TagHelperOutput output, ModelExpression model)
{
@@ -152,7 +161,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual AbpTagHelper GetSelectGroupTagHelper(TagHelperContext context, TagHelperOutput output, ModelExpression model)
{
- return IsRadioGroup(model.ModelExplorer) ?
+ return IsRadioGroup(model.ModelExplorer) ?
GetAbpRadioInputTagHelper(model) :
GetSelectTagHelper(model);
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs
index 10ce263032..f264d21cd5 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs
@@ -21,6 +21,9 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
public bool AutoFocus { get; set; }
+ [HtmlAttributeName("type")]
+ public string InputTypeName { get; set; }
+
public AbpFormControlSize Size { get; set; } = AbpFormControlSize.Default;
[HtmlAttributeName("required-symbol")]
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
index 75fe4f23e7..a2bb92a14d 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs
@@ -58,7 +58,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual async Task<(string, bool)> GetFormInputGroupAsHtmlAsync(TagHelperContext context, TagHelperOutput output)
{
var (inputTag, isCheckBox) = await GetInputTagHelperOutputAsync(context, output);
-
+
var inputHtml = inputTag.Render(_encoder);
var label = await GetLabelAsHtmlAsync(context, output, inputTag, isCheckBox);
var info = GetInfoAsHtml(context, output, inputTag, isCheckBox);
@@ -117,6 +117,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
return new InputTagHelper(_generator)
{
For = TagHelper.AspFor,
+ InputTypeName = TagHelper.InputTypeName,
ViewContext = TagHelper.ViewContext
};
}
@@ -161,7 +162,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual void AddDisabledAttribute(TagHelperOutput inputTagHelperOutput)
{
- if (inputTagHelperOutput.Attributes.ContainsName("disabled") == false &&
+ if (inputTagHelperOutput.Attributes.ContainsName("disabled") == false &&
(TagHelper.IsDisabled || TagHelper.AspFor.ModelExplorer.GetAttribute() != null))
{
inputTagHelperOutput.Attributes.Add("disabled", "");
@@ -170,7 +171,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
protected virtual void AddReadOnlyAttribute(TagHelperOutput inputTagHelperOutput)
{
- if (inputTagHelperOutput.Attributes.ContainsName("readonly") == false &&
+ if (inputTagHelperOutput.Attributes.ContainsName("readonly") == false &&
(TagHelper.IsReadonly != false || TagHelper.AspFor.ModelExplorer.GetAttribute() != null))
{
inputTagHelperOutput.Attributes.Add("readonly", "");
@@ -331,6 +332,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
var groupPrefix = "group-";
var tagHelperAttributes = output.Attributes.Where(a => !a.Name.StartsWith(groupPrefix)).ToList();
+
var attrList = new TagHelperAttributeList();
foreach (var tagHelperAttribute in tagHelperAttributes)
@@ -338,6 +340,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
attrList.Add(tagHelperAttribute);
}
+ if (!TagHelper.InputTypeName.IsNullOrEmpty() && !attrList.ContainsName("type"))
+ {
+ attrList.Add("type", TagHelper.InputTypeName);
+ }
+
return attrList;
}
@@ -400,9 +407,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
list.Add(new FormGroupItem
{
HtmlContent = html,
- Order = order
+ Order = order,
+ PropertyName = propertyName
});
}
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
index 6f71842b61..010cc1d075 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
@@ -167,9 +167,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
list.Add(new FormGroupItem
{
HtmlContent = html,
- Order = order
+ Order = order,
+ PropertyName = propertyName
});
}
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
index b18bc6ed75..3ed4f63f82 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpSelectTagHelperService.cs
@@ -125,7 +125,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
return await GetLabelAsHtmlUsingTagHelperAsync(context, output) + GetRequiredSymbol(context, output);
}
-
+
protected virtual string GetRequiredSymbol(TagHelperContext context, TagHelperOutput output)
{
if (!TagHelper.DisplayRequiredSymbol)
@@ -221,7 +221,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
}
protected virtual List GetSelectItemsFromAttribute(
- SelectItems selectItemsAttribute,
+ SelectItems selectItemsAttribute,
ModelExplorer explorer)
{
var selectItems = selectItemsAttribute.GetItems(explorer)?.ToList();
@@ -329,9 +329,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
list.Add(new FormGroupItem
{
HtmlContent = html,
- Order = order
+ Order = order,
+ PropertyName = propertyName
});
}
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/FormGroupItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/FormGroupItem.cs
index 258ba544f8..e3d33a4359 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/FormGroupItem.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/FormGroupItem.cs
@@ -5,5 +5,7 @@
public string HtmlContent { get; set; }
public int Order { get; set; }
+
+ public string PropertyName { get; set; }
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl.json b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl-PL.json
similarity index 92%
rename from framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl.json
rename to framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl-PL.json
index 8509b7d02b..438d770a7c 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/pl.json
+++ b/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",
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
index 77cae54b67..36f864e201 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
@@ -2,59 +2,16 @@
(function ($) {
- /************************************************************************
- * RECORD-ACTIONS extension for datatables
- ---------------------------------------------------------------
- * SINGLE BUTTON USAGE (creates the given JQuery element)
- {
- targets: 0, //optional
- rowAction:
- {
- element: $(" ")
- .addClass("btn btn-primary btn-sm m-btn--icon")
- .text("My button")
- .prepend($(" ").addClass("la la-sign-in"))
- .click(function () {
- console.log($(this).data());
- })
- },
- },
-
- ---------------------------------------------------------------
- * LIST OF ITEMS USAGE
- {
- targets: 0, //optional
- rowAction:
- {
- text: 'My actions', //optional. default value: Actions
- icon: 'bolt' //optional. default value: cog. See fa icon set https://fontawesome.com/v4.7.0/icons/
- items:
- [
- {
- text: "My first action", //mandatory
- icon: "thumbs-o-down", //optional.
- visible: true //optional. default value: true. Accepts boolean returning function too. Eg: function(){ return true/false;} ,
- action: function (data) {
- console.log(data.record);
- }
- },
- {
- text: "My second action",
- icon: "thumbs-o-up",
- visible: true,
- action: function (data) {
- console.log(data.record);
- }
- }
- ]
- }
- },
- *************************************************************************/
+ var datatables = abp.utils.createNamespace(abp, 'libs.datatables');
+
var localize = function (key) {
return abp.localization.getResource('AbpUi')(key);
};
- var recordActions = function () {
+ /************************************************************************
+ * RECORD-ACTIONS extension for datatables *
+ *************************************************************************/
+ (function () {
if (!$.fn.dataTableExt) {
return;
}
@@ -117,7 +74,7 @@
.addClass('action-button');
var $dropdownButton = $(' ');
-
+
if (field.icon !== undefined) {
if (field.icon) {
$dropdownButton.append($("").addClass("fa fa-" + field.icon + " mr-1"));
@@ -267,14 +224,12 @@
}
});
- }();
+ })();
/************************************************************************
* AJAX extension for datatables *
*************************************************************************/
- var datatables = abp.utils.createNamespace(abp, 'libs.datatables');
-
- var ajaxActions = function () {
+ (function () {
datatables.createAjax = function (serverMethod, inputAction) {
return function (requestData, callback, settings) {
var input = inputAction ? inputAction() : {};
@@ -318,14 +273,14 @@
}
};
};
- }();
+ })();
/************************************************************************
* Configuration/Options normalizer for datatables *
*************************************************************************/
- var optionNormalizer = function () {
+ (function () {
- var customizeRowActionColumn = function(column) {
+ var customizeRowActionColumn = function (column) {
column.data = null;
column.orderable = false;
column.defaultContent = "";
@@ -336,6 +291,9 @@
};
datatables.normalizeConfiguration = function (configuration) {
+
+ configuration.scrollX = true;
+
for (var i = 0; i < configuration.columnDefs.length; i++) {
var column = configuration.columnDefs[i];
if (!column.targets) {
@@ -369,6 +327,6 @@
return configuration;
};
- }();
+ })();
})(jQuery);
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
index 7e469f4195..00ba8423b3 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs
@@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Localization;
using Volo.Abp.ApiVersioning;
using Volo.Abp.AspNetCore.Mvc.ApiExploring;
using Volo.Abp.AspNetCore.Mvc.Conventions;
@@ -90,8 +91,11 @@ namespace Volo.Abp.AspNetCore.Mvc
var mvcCoreBuilder = context.Services.AddMvcCore();
context.Services.ExecutePreConfiguredActions(mvcCoreBuilder);
-
- var abpMvcDataAnnotationsLocalizationOptions = context.Services.ExecutePreConfiguredActions(new AbpMvcDataAnnotationsLocalizationOptions());
+
+ var abpMvcDataAnnotationsLocalizationOptions = context.Services
+ .ExecutePreConfiguredActions(
+ new AbpMvcDataAnnotationsLocalizationOptions()
+ );
context.Services
.AddSingleton>(
@@ -111,8 +115,17 @@ namespace Volo.Abp.AspNetCore.Mvc
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
- var resourceType = abpMvcDataAnnotationsLocalizationOptions.AssemblyResources.GetOrDefault(type.Assembly);
- return factory.Create(resourceType ?? type);
+ var resourceType = abpMvcDataAnnotationsLocalizationOptions
+ .AssemblyResources
+ .GetOrDefault(type.Assembly);
+
+ if (resourceType != null)
+ {
+ return factory.Create(resourceType);
+ }
+
+ return factory.CreateDefaultOrNull() ??
+ factory.Create(type);
};
})
.AddViewLocalization(); //TODO: How to configure from the application? Also, consider to move to a UI module since APIs does not care about it.
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
index f1225f1abf..4aa183a43d 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
@@ -5,6 +5,7 @@ using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features;
using Volo.Abp.AspNetCore.Mvc.ModelBinding;
+using Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata;
using Volo.Abp.AspNetCore.Mvc.Response;
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation;
@@ -48,6 +49,7 @@ namespace Volo.Abp.AspNetCore.Mvc
private static void AddModelBinders(MvcOptions options)
{
options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());
+ options.ModelBinderProviders.Insert(0, new AbpExtraPropertiesDictionaryModelBinderProvider());
}
private static void AddMetadataProviders(MvcOptions options, IServiceCollection services)
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
index 4b4cf521c2..187823b663 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
@@ -8,6 +8,7 @@ using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
+using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ObjectExtending;
using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
using Volo.Abp.Authorization;
using Volo.Abp.Features;
@@ -30,6 +31,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
private readonly ISettingDefinitionManager _settingDefinitionManager;
private readonly IFeatureDefinitionManager _featureDefinitionManager;
private readonly ILanguageProvider _languageProvider;
+ private readonly ICachedObjectExtensionsDtoService _cachedObjectExtensionsDtoService;
public AbpApplicationConfigurationAppService(
IOptions localizationOptions,
@@ -41,7 +43,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
ISettingProvider settingProvider,
ISettingDefinitionManager settingDefinitionManager,
IFeatureDefinitionManager featureDefinitionManager,
- ILanguageProvider languageProvider)
+ ILanguageProvider languageProvider,
+ ICachedObjectExtensionsDtoService cachedObjectExtensionsDtoService)
{
_serviceProvider = serviceProvider;
_abpAuthorizationPolicyProvider = abpAuthorizationPolicyProvider;
@@ -51,6 +54,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
_settingDefinitionManager = settingDefinitionManager;
_featureDefinitionManager = featureDefinitionManager;
_languageProvider = languageProvider;
+ _cachedObjectExtensionsDtoService = cachedObjectExtensionsDtoService;
_localizationOptions = localizationOptions.Value;
_multiTenancyOptions = multiTenancyOptions.Value;
}
@@ -67,12 +71,12 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
CurrentUser = GetCurrentUser(),
Setting = await GetSettingConfigAsync(),
MultiTenancy = GetMultiTenancy(),
- CurrentTenant = GetCurrentTenant()
-
+ CurrentTenant = GetCurrentTenant(),
+ ObjectExtensions = _cachedObjectExtensionsDtoService.Get()
};
}
- protected virtual CurrentTenantDto GetCurrentTenant()
+ protected virtual CurrentTenantDto GetCurrentTenant()
{
return new CurrentTenantDto()
{
@@ -154,6 +158,13 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
localizationConfig.CurrentCulture = GetCurrentCultureInfo();
+ if (_localizationOptions.DefaultResourceType != null)
+ {
+ localizationConfig.DefaultResourceName = LocalizationResourceNameAttribute.GetName(
+ _localizationOptions.DefaultResourceType
+ );
+ }
+
Logger.LogDebug("Executed AbpApplicationConfigurationAppService.GetLocalizationConfigAsync()");
return localizationConfig;
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
index f957fd894f..1042ee4d08 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
@@ -206,7 +206,7 @@ namespace Volo.Abp.AspNetCore.Mvc
/* TODO: Add interfaces
*/
- var typeName = ModelingTypeHelper.GetFullNameHandlingNullableAndGenerics(type);
+ var typeName = TypeHelper.GetFullNameHandlingNullableAndGenerics(type);
if (applicationModel.Types.ContainsKey(typeName))
{
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs
index 6428cf485e..9b376ab540 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs
@@ -2,7 +2,7 @@
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using System;
-using Volo.Abp.Settings;
+using Volo.Abp.Localization;
namespace Volo.Abp.AspNetCore.Mvc.Localization
{
@@ -13,7 +13,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Localization
[HttpGet]
public IActionResult Switch(string culture, string uiCulture = "", string returnUrl = "")
{
- if (!GlobalizationHelper.IsValidCultureCode(culture))
+ if (!CultureHelper.IsValidCultureCode(culture))
{
throw new AbpException("Unknown language: " + culture + ". It must be a valid culture!");
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/GlobalizationHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/GlobalizationHelper.cs
deleted file mode 100644
index a84faeb8f9..0000000000
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/GlobalizationHelper.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Globalization;
-
-namespace Volo.Abp.AspNetCore.Mvc.Localization
-{
- internal static class GlobalizationHelper
- {
- public static bool IsValidCultureCode(string cultureCode)
- {
- if (cultureCode.IsNullOrWhiteSpace())
- {
- return false;
- }
-
- try
- {
- CultureInfo.GetCultureInfo(cultureCode);
- return true;
- }
- catch (CultureNotFoundException)
- {
- return false;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs
new file mode 100644
index 0000000000..3ed1eda3a1
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertiesDictionaryModelBinderProvider.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Volo.Abp.Data;
+
+namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
+{
+ public class AbpExtraPropertiesDictionaryModelBinderProvider : IModelBinderProvider
+ {
+ public IModelBinder GetBinder(ModelBinderProviderContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.Metadata.ModelType != typeof(Dictionary))
+ {
+ return null;
+ }
+
+ if (!context.Metadata.ContainerType.IsAssignableTo())
+ {
+ return null;
+ }
+
+ var binderType = typeof(DictionaryModelBinder);
+ var keyBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(typeof(string)));
+ var valueBinder = new AbpExtraPropertyModelBinder(context.Metadata.ContainerType);
+ var loggerFactory = context.Services.GetRequiredService();
+ var mvcOptions = context.Services.GetRequiredService>().Value;
+
+ return (IModelBinder)Activator.CreateInstance(
+ binderType,
+ keyBinder,
+ valueBinder,
+ loggerFactory,
+ true /* allowValidatingTopLevelNodes */,
+ mvcOptions);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs
new file mode 100644
index 0000000000..d3f570b1ce
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpExtraPropertyModelBinder.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Volo.Abp.ObjectExtending;
+
+namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
+{
+ public class AbpExtraPropertyModelBinder : IModelBinder
+ {
+ public Type ExtensibleObjectType { get; }
+
+ public AbpExtraPropertyModelBinder(Type extensibleObjectType)
+ {
+ ExtensibleObjectType = extensibleObjectType;
+ }
+
+ public virtual Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ if (valueProviderResult == ValueProviderResult.None)
+ {
+ return Task.CompletedTask;
+ }
+
+ bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
+
+ var model = ConvertStringToPropertyType(
+ bindingContext,
+ valueProviderResult.FirstValue
+ );
+
+ bindingContext.Result = ModelBindingResult.Success(model);
+
+ return Task.CompletedTask;
+ }
+
+ protected virtual object ConvertStringToPropertyType(ModelBindingContext bindingContext, string value)
+ {
+ if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ var extensionInfo = ObjectExtensionManager.Instance.GetOrNull(ExtensibleObjectType);
+ if (extensionInfo == null)
+ {
+ return value;
+ }
+
+ var propertyName = ExtraPropertyBindingHelper.ExtractExtraPropertyName(bindingContext.ModelName);
+ if (propertyName == null)
+ {
+ return value;
+ }
+
+ var propertyInfo = extensionInfo.GetPropertyOrNull(propertyName);
+ if (propertyInfo == null)
+ {
+ return value;
+ }
+
+ return Convert.ChangeType(value, propertyInfo.Type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/ExtraPropertyBindingHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/ExtraPropertyBindingHelper.cs
new file mode 100644
index 0000000000..0bd4c43173
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/ExtraPropertyBindingHelper.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
+{
+ public static class ExtraPropertyBindingHelper
+ {
+ ///
+ /// is a string like "UserInfo.ExtraProperties[SocialSecurityNumber]"
+ /// This method returns "SocialSecurityNumber" for this example. */
+ ///
+ public static string ExtractExtraPropertyName(string expression)
+ {
+ var index = expression.IndexOf("ExtraProperties[", StringComparison.Ordinal);
+ if (index < 0)
+ {
+ return null;
+ }
+
+ return expression.Substring(index + 16, expression.Length - index - 17);
+ }
+
+ ///
+ /// is a string like "UserInfo.ExtraProperties[SocialSecurityNumber]"
+ /// This method returns "UserInfo" for this example.
+ ///
+ public static string ExtractContainerName(string expression)
+ {
+ var index = expression.IndexOf("ExtraProperties[", StringComparison.Ordinal);
+ if (index < 0)
+ {
+ return null;
+ }
+
+ return expression.Left(index).TrimEnd('.');
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs
index b77354aab2..94a08a0507 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs
@@ -1,11 +1,11 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Volo.Abp.AspNetCore.Mvc.Validation;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata
@@ -14,14 +14,6 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata
[ExposeServices(typeof(IModelMetadataProvider))]
public class AbpModelMetadataProvider : DefaultModelMetadataProvider
{
- private static readonly PropertyInfo ValidationAttributeErrorMessageStringProperty;
-
- static AbpModelMetadataProvider()
- {
- ValidationAttributeErrorMessageStringProperty = typeof(ValidationAttribute)
- .GetProperty("ErrorMessageString", BindingFlags.Instance | BindingFlags.NonPublic);
- }
-
public AbpModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider)
: base(detailsProvider)
{
@@ -56,7 +48,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ModelBinding.Metadata
{
if (validationAttribute.ErrorMessage == null)
{
- validationAttribute.ErrorMessage = ValidationAttributeErrorMessageStringProperty.GetValue(validationAttribute) as string;
+ ValidationAttributeHelper.SetDefaultErrorMessage(validationAttribute);
}
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs
new file mode 100644
index 0000000000..955b83c48f
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using System.Text;
+
+namespace Volo.Abp.AspNetCore.Mvc.Validation
+{
+ public static class ValidationAttributeHelper
+ {
+ private static readonly PropertyInfo ValidationAttributeErrorMessageStringProperty = typeof(ValidationAttribute)
+ .GetProperty("ErrorMessageString", BindingFlags.Instance | BindingFlags.NonPublic);
+
+ public static void SetDefaultErrorMessage(ValidationAttribute validationAttribute)
+ {
+ validationAttribute.ErrorMessage =
+ ValidationAttributeErrorMessageStringProperty.GetValue(validationAttribute) as string;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ViewFeatures/AbpValidationHtmlAttributeProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ViewFeatures/AbpValidationHtmlAttributeProvider.cs
new file mode 100644
index 0000000000..84a3d05e25
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ViewFeatures/AbpValidationHtmlAttributeProvider.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.DataAnnotations;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using Volo.Abp.AspNetCore.Mvc.ModelBinding;
+using Volo.Abp.AspNetCore.Mvc.Validation;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Localization;
+using Volo.Abp.ObjectExtending;
+using Volo.Abp.Validation.Localization;
+
+namespace Volo.Abp.AspNetCore.Mvc.ViewFeatures
+{
+ [Dependency(ReplaceServices = true)]
+ [ExposeServices(typeof(ValidationHtmlAttributeProvider))]
+ public class AbpValidationHtmlAttributeProvider
+ : DefaultValidationHtmlAttributeProvider, ISingletonDependency
+ {
+ private readonly IModelMetadataProvider _metadataProvider;
+ private readonly IStringLocalizerFactory _stringLocalizerFactory;
+ private readonly IStringLocalizer _validationStringLocalizer;
+ private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
+
+ public AbpValidationHtmlAttributeProvider(
+ IOptions optionsAccessor,
+ IModelMetadataProvider metadataProvider,
+ ClientValidatorCache clientValidatorCache,
+ IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
+ IStringLocalizerFactory stringLocalizerFactory,
+ IStringLocalizer validationStringLocalizer)
+ : base(
+ optionsAccessor,
+ metadataProvider,
+ clientValidatorCache)
+ {
+ _metadataProvider = metadataProvider;
+ _validationAttributeAdapterProvider = validationAttributeAdapterProvider;
+ _stringLocalizerFactory = stringLocalizerFactory;
+ _validationStringLocalizer = validationStringLocalizer;
+ }
+
+ public override void AddValidationAttributes(
+ ViewContext viewContext,
+ ModelExplorer modelExplorer,
+ IDictionary attributes)
+ {
+ base.AddValidationAttributes(viewContext, modelExplorer, attributes);
+ AddExtraPropertyValidationsAttributes(viewContext, modelExplorer, attributes);
+ }
+
+ protected virtual void AddExtraPropertyValidationsAttributes(ViewContext viewContext, ModelExplorer modelExplorer, IDictionary attributes)
+ {
+ var nameAttribute = attributes.GetOrDefault("name");
+ if (nameAttribute == null)
+ {
+ return;
+ }
+
+ var extraPropertyName = ExtraPropertyBindingHelper.ExtractExtraPropertyName(nameAttribute);
+ if (extraPropertyName == null)
+ {
+ return;
+ }
+
+ //TODO: containerName can be null on controller actions..?
+ var containerName = ExtraPropertyBindingHelper.ExtractContainerName(nameAttribute);
+ if (containerName == null)
+ {
+ return;
+ }
+
+ if (modelExplorer.Container?.ModelType == null)
+ {
+ return;
+ }
+
+ var extensibleObjectType = modelExplorer.Container.ModelType
+ .GetProperty(containerName, BindingFlags.Instance | BindingFlags.Public)
+ ?.PropertyType;
+ if (extensibleObjectType == null)
+ {
+ return;
+ }
+
+ var extensionPropertyInfo = ObjectExtensionManager.Instance.GetPropertyOrNull(
+ extensibleObjectType,
+ extraPropertyName
+ );
+
+ if (extensionPropertyInfo == null)
+ {
+ return;
+ }
+
+ if (modelExplorer.Metadata is DefaultModelMetadata metadata)
+ {
+ metadata.DisplayMetadata.DisplayName =
+ () => extensionPropertyInfo.GetLocalizedDisplayName(_stringLocalizerFactory);
+ }
+
+ foreach (var validationAttribute in extensionPropertyInfo.GetValidationAttributes())
+ {
+ var validationContext = new ClientModelValidationContext(
+ viewContext,
+ modelExplorer.Metadata,
+ _metadataProvider,
+ attributes
+ );
+
+ if (validationAttribute.ErrorMessage == null)
+ {
+ ValidationAttributeHelper.SetDefaultErrorMessage(validationAttribute);
+ }
+
+ var validationAttributeAdapter = _validationAttributeAdapterProvider.GetAttributeAdapter(
+ validationAttribute,
+ _validationStringLocalizer
+ );
+
+ validationAttributeAdapter?.AddValidation(validationContext);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs
new file mode 100644
index 0000000000..9bb6c903a9
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/ObjectExtending/ObjectExtendingPropertyInfoExtensions.cs
@@ -0,0 +1,98 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Volo.Abp.ObjectExtending
+{
+ public static class ObjectExtensionPropertyInfoAspNetCoreMvcExtensions
+ {
+ public static string GetInputType(this ObjectExtensionPropertyInfo propertyInfo)
+ {
+ foreach (var attribute in propertyInfo.Attributes)
+ {
+ var inputTypeByAttribute = GetInputTypeFromAttributeOrNull(attribute);
+ if (inputTypeByAttribute != null)
+ {
+ return inputTypeByAttribute;
+ }
+ }
+
+ return GetInputTypeFromTypeOrNull(propertyInfo.Type)
+ ?? "text"; //default
+ }
+
+ private static string GetInputTypeFromAttributeOrNull(Attribute attribute)
+ {
+ if (attribute is EmailAddressAttribute)
+ {
+ return "email";
+ }
+
+ if (attribute is UrlAttribute)
+ {
+ return "url";
+ }
+
+ if (attribute is HiddenInputAttribute)
+ {
+ return "hidden";
+ }
+
+ if (attribute is PhoneAttribute)
+ {
+ return "tel";
+ }
+
+ if (attribute is DataTypeAttribute dataTypeAttribute)
+ {
+ switch (dataTypeAttribute.DataType)
+ {
+ case DataType.Password:
+ return "password";
+ case DataType.Date:
+ return "date";
+ case DataType.Time:
+ return "time";
+ case DataType.EmailAddress:
+ return "email";
+ case DataType.Url:
+ return "url";
+ case DataType.PhoneNumber:
+ return "tel";
+ case DataType.DateTime:
+ return "datetime-local";
+ }
+ }
+
+ return null;
+ }
+
+ private static string GetInputTypeFromTypeOrNull(Type type)
+ {
+ if (type == typeof(bool))
+ {
+ return "checkbox";
+ }
+
+ if (type == typeof(DateTime))
+ {
+ return "datetime-local";
+ }
+
+ if (type == typeof(int) ||
+ type == typeof(long) ||
+ type == typeof(byte) ||
+ type == typeof(sbyte) ||
+ type == typeof(short) ||
+ type == typeof(ushort) ||
+ type == typeof(uint) ||
+ type == typeof(long) ||
+ type == typeof(ulong))
+ {
+ return "number";
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
index e6a2319c72..ca8bfa9bf8 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
@@ -57,7 +57,11 @@ namespace Volo.Abp.Cli.Commands
.Options
.GetOrNull(Options.IncludePreviews.Short, Options.IncludePreviews.Long) != null;
- var solution = Directory.GetFiles(directory, "*.sln", SearchOption.AllDirectories).FirstOrDefault();
+ var solution = commandLineArgs.Options.GetOrNull(Options.SolutionName.Short, Options.SolutionName.Long);
+ if (solution.IsNullOrWhiteSpace())
+ {
+ solution = Directory.GetFiles(directory, "*.sln", SearchOption.AllDirectories).FirstOrDefault();
+ }
if (solution != null)
{
@@ -101,11 +105,14 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine("-p|--include-previews (if supported by the template)");
sb.AppendLine("--npm (Only updates NPM packages)");
sb.AppendLine("--nuget (Only updates Nuget packages)");
+ sb.AppendLine("-sp|--solution-path (Specify the solution path)");
+ sb.AppendLine("-sn|--solution-name (Specify the solution name)");
sb.AppendLine("");
sb.AppendLine("Some examples:");
sb.AppendLine("");
sb.AppendLine(" abp update");
sb.AppendLine(" abp update -p");
+ sb.AppendLine(" abp update -sp \"D:\\projects\\\" -sn Acme.BookStore");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
@@ -125,6 +132,12 @@ namespace Volo.Abp.Cli.Commands
public const string Long = "solution-path";
}
+ public static class SolutionName
+ {
+ public const string Short = "sn";
+ public const string Long = "solution-name";
+ }
+
public static class IncludePreviews
{
public const string Short = "p";
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs
index ca1a23fcfc..bd3037d830 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs
+++ b/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 GetHttpResponseMessageWithRetryAsync
+ (
+ string url,
+ CancellationToken? cancellationToken = null,
+ ILogger logger = null,
+ IEnumerable 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));
+ }
+
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/AbpIoApiKeyService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/AbpIoApiKeyService.cs
index cddead548f..33a2e10fdf 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/AbpIoApiKeyService.cs
+++ b/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 _logger;
private DeveloperApiKeyResult _apiKeyResult = null;
- public AbpIoApiKeyService(IJsonSerializer jsonSerializer, IRemoteServiceExceptionHandler remoteServiceExceptionHandler, ILogger logger)
+ public AbpIoApiKeyService(
+ IJsonSerializer jsonSerializer,
+ ICancellationTokenProvider cancellationTokenProvider,
+ IRemoteServiceExceptionHandler remoteServiceExceptionHandler,
+ ILogger logger)
{
JsonSerializer = jsonSerializer;
RemoteServiceExceptionHandler = remoteServiceExceptionHandler;
_logger = logger;
+ CancellationTokenProvider = cancellationTokenProvider;
}
public async Task 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)
{
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs
index c0eb6fd209..0fd35582bf 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs
+++ b/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 _proPackageList;
+ private DeveloperApiKeyResult _apiKeyResult;
public NuGetService(
IJsonSerializer jsonSerializer,
@@ -45,20 +41,20 @@ namespace Volo.Abp.Cli.NuGet
public async Task GetLatestVersionOrNullAsync(string packageId, bool includePreviews = false, bool includeNightly = false)
{
- List 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 GetNuGetUrlForCommercialPackage(string packageId)
{
- var apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync();
- return CliUrls.GetNuGetPackageInfoUrl(apiKeyResult.ApiKey, packageId);
- }
+ if (_apiKeyResult == null)
+ {
+ _apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync();
+ }
- private async Task 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> 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>(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>(await responseMessage.Content.ReadAsStringAsync());
+ Logger.LogError(exceptionMessage);
+ return null;
}
public class NuGetVersionResultDto
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs
index 41907276f4..75e9156ce0 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/MyGetPackageListFinder.cs
@@ -20,7 +20,7 @@ namespace Volo.Abp.Cli.ProjectModification
Logger = NullLogger.Instance;
}
- public async Task GetPackages()
+ public async Task GetPackagesAsync()
{
if (_response != null)
{
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
index a5a2e506cc..1a279fafc7 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
@@ -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 Logger { get; set; }
+ protected ICancellationTokenProvider CancellationTokenProvider { get; }
private readonly PackageJsonFileFinder _packageJsonFileFinder;
private readonly NpmGlobalPackagesChecker _npmGlobalPackagesChecker;
private readonly MyGetPackageListFinder _myGetPackageListFinder;
-
private readonly Dictionary _fileVersionStorage = new Dictionary();
+ 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.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 UpdatePackagesInFile(string file, bool includePreviews = false, bool switchToStable = false)
+ protected virtual async Task 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 TryUpdatePackage(string file, JProperty package,
- bool includePreviews = false, bool switchToStable = false)
+ protected virtual async Task 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 GetLatestVersion(JProperty package, string currentVersion,
- bool includePreviews = false, bool switchToStable = false)
+ protected virtual async Task 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;
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
index 587492804b..fb2a386ef9 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
@@ -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 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 GetLatestVersionFromMyGet(string packageId)
+ {
+ var myGetPack = await _myGetPackageListFinder.GetPackagesAsync();
+
+ return myGetPack.Packages.FirstOrDefault(p => p.Id == packageId)?.Versions.LastOrDefault();
+ }
}
}
diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs
index 34931bfc95..3995bf4440 100644
--- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs
+++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs
@@ -1,6 +1,6 @@
namespace Microsoft.Extensions.Localization
{
- public static class AbpStringLocalizerFactoryExtensions
+ public static class AbpCoreStringLocalizerFactoryExtensions
{
public static IStringLocalizer Create(this IStringLocalizerFactory localizerFactory)
{
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs
index 2ddf9233ba..6957a5442c 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Globalization;
using System.Threading;
using JetBrains.Annotations;
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
index 5f66f4788e..11f43f852e 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
+using JetBrains.Annotations;
namespace Volo.Abp.Reflection
{
@@ -131,5 +132,137 @@ namespace Volo.Abp.Reflection
type == typeof(TimeSpan) ||
type == typeof(Guid);
}
+
+ public static T GetDefaultValue()
+ {
+ return default;
+ }
+
+ public static object GetDefaultValue(Type type)
+ {
+ if (type.IsValueType)
+ {
+ return Activator.CreateInstance(type);
+ }
+
+ return null;
+ }
+
+ public static string GetFullNameHandlingNullableAndGenerics([NotNull] Type type)
+ {
+ Check.NotNull(type, nameof(type));
+
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ return type.GenericTypeArguments[0].FullName + "?";
+ }
+
+ if (type.IsGenericType)
+ {
+ var genericType = type.GetGenericTypeDefinition();
+ return $"{genericType.FullName.Left(genericType.FullName.IndexOf('`'))}<{type.GenericTypeArguments.Select(GetFullNameHandlingNullableAndGenerics).JoinAsString(",")}>";
+ }
+
+ return type.FullName;
+ }
+
+ public static string GetSimplifiedName([NotNull] Type type)
+ {
+ Check.NotNull(type, nameof(type));
+
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ return GetSimplifiedName(type.GenericTypeArguments[0]) + "?";
+ }
+
+ if (type.IsGenericType)
+ {
+ var genericType = type.GetGenericTypeDefinition();
+ return $"{genericType.FullName.Left(genericType.FullName.IndexOf('`'))}<{type.GenericTypeArguments.Select(GetSimplifiedName).JoinAsString(",")}>";
+ }
+
+ if (type == typeof(string))
+ {
+ return "string";
+ }
+ else if (type == typeof(int))
+ {
+ return "number";
+ }
+ else if (type == typeof(long))
+ {
+ return "number";
+ }
+ else if (type == typeof(bool))
+ {
+ return "boolean";
+ }
+ else if (type == typeof(char))
+ {
+ return "string";
+ }
+ else if (type == typeof(double))
+ {
+ return "number";
+ }
+ else if (type == typeof(float))
+ {
+ return "number";
+ }
+ else if (type == typeof(decimal))
+ {
+ return "number";
+ }
+ else if (type == typeof(DateTime))
+ {
+ return "string";
+ }
+ else if (type == typeof(DateTimeOffset))
+ {
+ return "string";
+ }
+ else if (type == typeof(TimeSpan))
+ {
+ return "string";
+ }
+ else if (type == typeof(Guid))
+ {
+ return "string";
+ }
+ else if (type == typeof(byte))
+ {
+ return "number";
+ }
+ else if (type == typeof(sbyte))
+ {
+ return "number";
+ }
+ else if (type == typeof(short))
+ {
+ return "number";
+ }
+ else if (type == typeof(ushort))
+ {
+ return "number";
+ }
+ else if (type == typeof(uint))
+ {
+ return "number";
+ }
+ else if (type == typeof(ulong))
+ {
+ return "number";
+ }
+ else if (type == typeof(IntPtr))
+ {
+ return "number";
+ }
+ else if (type == typeof(UIntPtr))
+ {
+ return "number";
+ }
+
+ return type.FullName;
+ }
}
}
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs
index be02f3d398..b0c20131ef 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs
@@ -25,8 +25,9 @@ namespace Volo.Abp.Domain.Entities
protected AggregateRoot()
{
- ExtraProperties = new Dictionary();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
+ ExtraProperties = new Dictionary();
+ this.SetDefaultsForExtraProperties();
}
protected virtual void AddLocalEvent(object eventData)
@@ -85,15 +86,17 @@ namespace Volo.Abp.Domain.Entities
protected AggregateRoot()
{
- ExtraProperties = new Dictionary();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
+ ExtraProperties = new Dictionary();
+ this.SetDefaultsForExtraProperties();
}
protected AggregateRoot(TKey id)
: base(id)
{
- ExtraProperties = new Dictionary();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
+ ExtraProperties = new Dictionary();
+ this.SetDefaultsForExtraProperties();
}
protected virtual void AddLocalEvent(object eventData)
diff --git a/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj b/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj
index be953a1f26..a2f83399a0 100644
--- a/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj
+++ b/framework/src/Volo.Abp.Emailing/Volo.Abp.Emailing.csproj
@@ -14,24 +14,21 @@
-
-
-
-
-
-
+
+
+
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/AbpEmailingModule.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/AbpEmailingModule.cs
index c2e29a7ff7..afcbaf6aef 100644
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/AbpEmailingModule.cs
+++ b/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(options =>
@@ -43,41 +36,5 @@ namespace Volo.Abp.Emailing
options.AddJob();
});
}
-
- private static void AutoAddDefinitionProviders(IServiceCollection services)
- {
- var definitionProviders = new List();
-
- services.OnRegistred(context =>
- {
-
- if (typeof(IEmailTemplateDefinitionProvider).IsAssignableFrom(context.ImplementationType))
- {
- definitionProviders.Add(context.ImplementationType);
- }
- });
-
- services.Configure(options =>
- {
- options.DefinitionProviders.AddIfNotContains(definitionProviders);
- });
- }
-
- public override void OnApplicationInitialization(ApplicationInitializationContext context)
- {
- using (var scope = context.ServiceProvider.CreateScope())
- {
- var emailTemplateDefinitionManager =
- scope.ServiceProvider.GetRequiredService();
-
- foreach (var templateDefinition in emailTemplateDefinitionManager.GetAll())
- {
- foreach (var contributor in templateDefinition.Contributors)
- {
- contributor.Initialize(new EmailTemplateInitializationContext(templateDefinition, scope.ServiceProvider));
- }
- }
- }
- }
}
}
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/AbpEmailTemplateOptions.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/AbpEmailTemplateOptions.cs
deleted file mode 100644
index cb5a9d370b..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/AbpEmailTemplateOptions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Volo.Abp.Collections;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public class AbpEmailTemplateOptions
- {
- public string DefaultLayout { get; set; }
-
- public ITypeList DefinitionProviders { get; }
-
- public AbpEmailTemplateOptions()
- {
- DefaultLayout = StandardEmailTemplates.DefaultLayout;
-
- DefinitionProviders = new TypeList();
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplateProvider.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplateProvider.cs
index c15012f0bf..385e0ef178 100644
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplateProvider.cs
+++ b/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")
+ );
}
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message/en.tpl b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message/en.tpl
deleted file mode 100644
index 6454b0b93a..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message/en.tpl
+++ /dev/null
@@ -1 +0,0 @@
-{{message}}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplate.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplate.cs
deleted file mode 100644
index ad6f8c4839..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplate.cs
+++ /dev/null
@@ -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);
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateContributorList.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateContributorList.cs
deleted file mode 100644
index 44a91212ea..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateContributorList.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public class EmailTemplateContributorList : List
- {
- public string GetOrNull(string cultureName)
- {
- foreach (var contributor in this.AsQueryable().Reverse())
- {
- var templateString = contributor.GetOrNull(cultureName);
- if (templateString != null)
- {
- return templateString;
- }
- }
-
- return null;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinition.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinition.cs
deleted file mode 100644
index 19f0eb2fa7..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinition.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionContext.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionContext.cs
deleted file mode 100644
index 03a6c95d8b..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionContext.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.Immutable;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public class EmailTemplateDefinitionContext : IEmailTemplateDefinitionContext
- {
- protected Dictionary EmailTemplates { get; }
-
- public EmailTemplateDefinitionContext(Dictionary emailTemplates)
- {
- EmailTemplates = emailTemplates;
- }
-
- public virtual EmailTemplateDefinition GetOrNull(string name)
- {
- return EmailTemplates.GetOrDefault(name);
- }
-
- public virtual IReadOnlyList 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;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionDictionary.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionDictionary.cs
deleted file mode 100644
index aa36232156..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionDictionary.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Collections.Generic;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public class EmailTemplateDefinitionDictionary : Dictionary
- {
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionManager.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionManager.cs
deleted file mode 100644
index 0491dc867e..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionManager.cs
+++ /dev/null
@@ -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> EmailTemplateDefinitions { get; }
-
- protected AbpEmailTemplateOptions Options { get; }
-
- protected IServiceProvider ServiceProvider { get; }
-
- public EmailTemplateDefinitionManager(
- IOptions options,
- IServiceProvider serviceProvider)
- {
- ServiceProvider = serviceProvider;
- Options = options.Value;
-
- EmailTemplateDefinitions =
- new Lazy>(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 GetAll()
- {
- return EmailTemplateDefinitions.Value.Values.ToImmutableList();
- }
-
- public virtual EmailTemplateDefinition GetOrNull(string name)
- {
- return EmailTemplateDefinitions.Value.GetOrDefault(name);
- }
-
- protected virtual IDictionary CreateEmailTemplateDefinitions()
- {
- var templates = new Dictionary();
-
- 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;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionProvider.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionProvider.cs
deleted file mode 100644
index e53505fafc..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateDefinitionProvider.cs
+++ /dev/null
@@ -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);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateInitializationContext.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateInitializationContext.cs
deleted file mode 100644
index 8cd4b95bbd..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateInitializationContext.cs
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateProvider.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateProvider.cs
deleted file mode 100644
index 29fcf08664..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/EmailTemplateProvider.cs
+++ /dev/null
@@ -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 options)
- {
- EmailTemplateDefinitionManager = emailTemplateDefinitionManager;
- TemplateLocalizer = templateLocalizer;
- StringLocalizerFactory = stringLocalizerFactory;
- Options = options.Value;
- }
-
- public async Task GetAsync(string name)
- {
- return await GetAsync(name, CultureInfo.CurrentUICulture.Name);
- }
-
- public async Task GetAsync(string name, string cultureName)
- {
- return await GetInternalAsync(name, cultureName);
- }
-
- protected virtual async Task 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;
- }
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateContributor.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateContributor.cs
deleted file mode 100644
index d2c2775845..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateContributor.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Volo.Abp.Emailing.Templates
-{
- public interface IEmailTemplateContributor
- {
- void Initialize(EmailTemplateInitializationContext context);
-
- string GetOrNull(string cultureName);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionContext.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionContext.cs
deleted file mode 100644
index 1641562ccf..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionContext.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Volo.Abp.Emailing.Templates
-{
- public interface IEmailTemplateDefinitionContext
- {
- EmailTemplateDefinition GetOrNull(string name);
-
- void Add(params EmailTemplateDefinition[] definitions);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionManager.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionManager.cs
deleted file mode 100644
index 0936a2fe93..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionManager.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public interface IEmailTemplateDefinitionManager
- {
- [NotNull]
- EmailTemplateDefinition Get([NotNull] string name);
-
- IReadOnlyList GetAll();
-
- EmailTemplateDefinition GetOrNull(string name);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionProvider.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionProvider.cs
deleted file mode 100644
index 691d3874d6..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateDefinitionProvider.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Volo.Abp.Emailing.Templates
-{
- public interface IEmailTemplateDefinitionProvider
- {
- void Define(IEmailTemplateDefinitionContext context);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateProvider.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateProvider.cs
deleted file mode 100644
index ab68dbe2ca..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/IEmailTemplateProvider.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public interface IEmailTemplateProvider
- {
- Task GetAsync(string name);
-
- Task GetAsync(string name, string cultureName);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/ITemplateRender.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/ITemplateRender.cs
deleted file mode 100644
index 35ac14c8fd..0000000000
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/ITemplateRender.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Threading.Tasks;
-
-namespace Volo.Abp.Emailing.Templates
-{
- public interface ITemplateRender
- {
- Task RenderAsync(string template, object model = null);
- }
-}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout/en.tpl b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/Layout/en.tpl
similarity index 84%
rename from framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout/en.tpl
rename to framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/Layout/en.tpl
index 107fbb5230..57453a027f 100644
--- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout/en.tpl
+++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Templates/Layout/en.tpl
@@ -4,6 +4,6 @@
- {{#content}}
+ {{content}}