|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@ -1 +1,198 @@ |
|||||
TODO .... |
## ASP.NET Core {{UI_Value}} 教程 - 第三章 |
||||
|
````json |
||||
|
//[doc-params] |
||||
|
{ |
||||
|
"UI": ["MVC","NG"] |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
{{ |
||||
|
if UI == "MVC" |
||||
|
DB="ef" |
||||
|
DB_Text="Entity Framework Core" |
||||
|
UI_Text="mvc" |
||||
|
else if UI == "NG" |
||||
|
DB="mongodb" |
||||
|
DB_Text="MongoDB" |
||||
|
UI_Text="angular" |
||||
|
else |
||||
|
DB ="?" |
||||
|
UI_Text="?" |
||||
|
end |
||||
|
}} |
||||
|
|
||||
|
### 关于本教程 |
||||
|
|
||||
|
这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: |
||||
|
|
||||
|
- [Part-1: 创建项目和书籍列表页面](Part-1.md) |
||||
|
- [Part 2: 创建,编辑,删除书籍](Part-2.md) |
||||
|
- **Part-3: 集成测试(本章)** |
||||
|
|
||||
|
> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). |
||||
|
|
||||
|
### 解决方案中的测试项目 |
||||
|
|
||||
|
解决方案中有多个测试项目: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: |
||||
|
|
||||
|
* [xunit](https://xunit.github.io/) 作为主测试框架. |
||||
|
* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. |
||||
|
* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. |
||||
|
|
||||
|
### 添加测试数据 |
||||
|
|
||||
|
启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. |
||||
|
更改`BookStoreTestDataSeedContributor`类如下所示: |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.Guids; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookStoreTestDataSeedContributor |
||||
|
: IDataSeedContributor, ITransientDependency |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
private readonly IGuidGenerator _guidGenerator; |
||||
|
|
||||
|
public BookStoreTestDataSeedContributor( |
||||
|
IRepository<Book, Guid> bookRepository, |
||||
|
IGuidGenerator guidGenerator) |
||||
|
{ |
||||
|
_bookRepository = bookRepository; |
||||
|
_guidGenerator = guidGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task SeedAsync(DataSeedContext context) |
||||
|
{ |
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book(id: _guidGenerator.Create(), |
||||
|
name: "Test book 1", |
||||
|
type: BookType.Fantastic, |
||||
|
publishDate: new DateTime(2015, 05, 24), |
||||
|
price: 21 |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
await _bookRepository.InsertAsync( |
||||
|
new Book(id: _guidGenerator.Create(), |
||||
|
name: "Test book 2", |
||||
|
type: BookType.Science, |
||||
|
publishDate: new DateTime(2014, 02, 11), |
||||
|
price: 15 |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 注入`IRepository<Book,Guid>`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. |
||||
|
* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息). |
||||
|
|
||||
|
### 测试 BookAppService |
||||
|
|
||||
|
在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Xunit; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Validation; |
||||
|
using Microsoft.EntityFrameworkCore.Internal; |
||||
|
|
||||
|
namespace Acme.BookStore |
||||
|
{ |
||||
|
public class BookAppService_Tests : BookStoreApplicationTestBase |
||||
|
{ |
||||
|
private readonly IBookAppService _bookAppService; |
||||
|
|
||||
|
public BookAppService_Tests() |
||||
|
{ |
||||
|
_bookAppService = GetRequiredService<IBookAppService>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Get_List_Of_Books() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.GetListAsync( |
||||
|
new PagedAndSortedResultRequestDto() |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.TotalCount.ShouldBeGreaterThan(0); |
||||
|
result.Items.ShouldContain(b => b.Name == "Test book 1"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. |
||||
|
|
||||
|
新增测试方法,用以测试创建一个合法book实体的场景: |
||||
|
|
||||
|
````C# |
||||
|
[Fact] |
||||
|
public async Task Should_Create_A_Valid_Book() |
||||
|
{ |
||||
|
//Act |
||||
|
var result = await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "New test book 42", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
//Assert |
||||
|
result.Id.ShouldNotBe(Guid.Empty); |
||||
|
result.Name.ShouldBe("New test book 42"); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
新增测试方法,用以测试创建一个非法book实体失败的场景: |
||||
|
|
||||
|
````csharp |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Create_A_Book_Without_Name() |
||||
|
{ |
||||
|
var exception = await Assert.ThrowsAsync<Volo.Abp.Validation.AbpValidationException>(async () => |
||||
|
{ |
||||
|
await _bookAppService.CreateAsync( |
||||
|
new CreateUpdateBookDto |
||||
|
{ |
||||
|
Name = "", |
||||
|
Price = 10, |
||||
|
PublishDate = DateTime.Now, |
||||
|
Type = BookType.ScienceFiction |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
exception.ValidationErrors |
||||
|
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. |
||||
|
|
||||
|
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
恭喜, 绿色图标表示测试已成功通过! |
||||
|
|||||
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 810 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,197 @@ |
|||||
|
# 修改菜单 |
||||
|
|
||||
|
|
||||
|
菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局. |
||||
|
|
||||
|
<!-- TODO: Replace layout replacement document with component replacement. Layout replacement document will be created.--> |
||||
|
|
||||
|
## 如何添加Logo |
||||
|
|
||||
|
环境变量中的 `logoUrl` 是logo的url. |
||||
|
|
||||
|
你可以在 `src/assets` 文件夹下添加logo并设置 `logoUrl`: |
||||
|
|
||||
|
```js |
||||
|
export const environment = { |
||||
|
// other configurations |
||||
|
application: { |
||||
|
name: 'MyProjectName', |
||||
|
logoUrl: 'assets/logo.png', |
||||
|
}, |
||||
|
// other configurations |
||||
|
}; |
||||
|
``` |
||||
|
|
||||
|
## 如何添加导航元素 |
||||
|
|
||||
|
### 通过 AppRoutingModule 中的 `routes` 属性 |
||||
|
|
||||
|
你可以通过在 `app-routing.module` 中将路由作为子属性添加到路由配置的 `data` 属性来定义路由. `@abp/ng.core` 包组织路由并将其存储在 `ConfigState` 中.`ApplicationLayoutComponent` 从存储中获取路由显示在菜单上. |
||||
|
|
||||
|
你可以像以下一样添加 `routes` 属性: |
||||
|
|
||||
|
```js |
||||
|
{ |
||||
|
path: 'your-path', |
||||
|
data: { |
||||
|
routes: { |
||||
|
name: 'Your navigation', |
||||
|
order: 3, |
||||
|
iconClass: 'fas fa-question-circle', |
||||
|
requiredPolicy: 'permission key here', |
||||
|
children: [ |
||||
|
{ |
||||
|
path: 'child', |
||||
|
name: 'Your child navigation', |
||||
|
order: 1, |
||||
|
requiredPolicy: 'permission key here', |
||||
|
}, |
||||
|
], |
||||
|
} as ABP.Route, // can be imported from @abp/ng.core |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `name` 是导航元素的标签,可以传递本地化密钥或本地化对象. |
||||
|
- `order` 排序导航元素. |
||||
|
- `iconClass` 是 `i` 标签的类,在导航标签的左侧. |
||||
|
- `requiredPolicy` 是访问页面所需的权限key. 参阅 [权限管理文档](./Permission-Management.md) |
||||
|
- `children` is an array and is used for declaring child navigation elements. The child navigation element will be placed as a child route which will be available at `'/your-path/child'` based on the given `path` property. |
||||
|
- `children` 是一个数组,用于声明子菜单,它基于给定的 `path` 属性,路径是在`/your-path/child`. |
||||
|
|
||||
|
添加了上面描述的route属性后,导航菜单如下图所示: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 通过 ConfigState |
||||
|
|
||||
|
`ConfigStateService` 的 `dispatchAddRoute` 方法可以向菜单添加一个新的导航元素. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 2, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage', |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
`newRoute` 放在根级别,没有任何父路由,url将为`/path`. |
||||
|
|
||||
|
如果你想 **添加子路由, 你可以这样做:** |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRoute: ABP.Route = { |
||||
|
parentName: eIdentityRouteNames.IdentityManagement, |
||||
|
name: 'My New Page', |
||||
|
iconClass: 'fa fa-dashboard', |
||||
|
path: 'page', |
||||
|
invisible: false, |
||||
|
order: 3, |
||||
|
requiredPolicy: 'MyProjectName.MyNewPage' |
||||
|
} as Omit<ABP.Route, 'children'>; |
||||
|
|
||||
|
this.config.dispatchAddRoute(newRoute); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
`newRoute` 做为 `eIdentityRouteNames.IdentityManagement` 的子路由添加, url 设置为 `'/identity/page'`. |
||||
|
|
||||
|
新路由看起来像这样: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何修改一个导航元素 |
||||
|
|
||||
|
`DispatchPatchRouteByName` 方法通过名称查找路由,并使用二个参数传递的新配置替换存储中的配置. |
||||
|
|
||||
|
```js |
||||
|
// this.config is instance of ConfigStateService |
||||
|
// eIdentityRouteNames enum can be imported from @abp/ng.identity |
||||
|
|
||||
|
const newRouteConfig: Partial<ABP.Route> = { |
||||
|
iconClass: 'fas fa-home', |
||||
|
parentName: eIdentityRouteNames.Administration, |
||||
|
order: 0, |
||||
|
children: [ |
||||
|
{ |
||||
|
name: 'Dashboard', |
||||
|
path: 'dashboard', |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
this.config.dispatchPatchRouteByName('::Menu:Home', newRouteConfig); |
||||
|
// returns a state stream which emits after dispatch action is complete |
||||
|
``` |
||||
|
|
||||
|
* 根据给定的 `parentName` 将 _Home_ 导航移动到 _Administration_ 下拉框下. |
||||
|
* 添加了 icon. |
||||
|
* 指定了顺序. |
||||
|
* 添加了名为 _Dashboard_ 的子路由. |
||||
|
|
||||
|
修改后,导航元素看起来像这样: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何在菜单的右侧添加元素 |
||||
|
|
||||
|
右侧的元素存储在 @abp/ng.theme.basic 包的 `LayoutState` 中. |
||||
|
|
||||
|
`LayoutStateService` 的 `dispatchAddNavigationElement` 方法添加元素到右侧的菜单. |
||||
|
|
||||
|
你可以通过将模板添加到 `app.component` 调用 `dispatchAddNavigationElement` 方法来插入元素: |
||||
|
|
||||
|
```js |
||||
|
import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-root', |
||||
|
template: ` |
||||
|
|
||||
|
<!-- Added below content --> |
||||
|
<ng-template #search |
||||
|
><input type="search" placeholder="Search" class="bg-transparent border-0" |
||||
|
/></ng-template> |
||||
|
`, |
||||
|
}) |
||||
|
export class AppComponent { |
||||
|
// Added ViewChild |
||||
|
@ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef<any>; |
||||
|
|
||||
|
constructor(private layout: LayoutStateService) {} // injected LayoutStateService |
||||
|
|
||||
|
// Added ngAfterViewInit |
||||
|
ngAfterViewInit() { |
||||
|
const newElement = { |
||||
|
name: 'Search', |
||||
|
element: this.searchElementRef, |
||||
|
order: 1, |
||||
|
} as Layout.NavigationElement; |
||||
|
|
||||
|
this.layout.dispatchAddNavigationElement(newElement); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
上面我们在菜单添加了一个搜索输入,最终UI如下:s |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## 如何删除右侧菜单元素 |
||||
|
|
||||
|
TODO |
||||
|
|
||||
|
## 下一步是什么? |
||||
|
|
||||
|
* [组件替换](./Component-Replacement.md) |
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 44 KiB |
@ -1,5 +1,5 @@ |
|||||
{ |
{ |
||||
"culture": "pl", |
"culture": "pl-PL", |
||||
"texts": { |
"texts": { |
||||
"GivenTenantIsNotAvailable": "Podany tenant jest niedostępny: {0}", |
"GivenTenantIsNotAvailable": "Podany tenant jest niedostępny: {0}", |
||||
"Tenant": "Tenant", |
"Tenant": "Tenant", |
||||
@ -1,18 +0,0 @@ |
|||||
using Volo.Abp.Collections; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class AbpEmailTemplateOptions |
|
||||
{ |
|
||||
public string DefaultLayout { get; set; } |
|
||||
|
|
||||
public ITypeList<IEmailTemplateDefinitionProvider> DefinitionProviders { get; } |
|
||||
|
|
||||
public AbpEmailTemplateOptions() |
|
||||
{ |
|
||||
DefaultLayout = StandardEmailTemplates.DefaultLayout; |
|
||||
|
|
||||
DefinitionProviders = new TypeList<IEmailTemplateDefinitionProvider>(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +1,26 @@ |
|||||
using Volo.Abp.Emailing.Templates.VirtualFiles; |
using Volo.Abp.TextTemplating; |
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
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) |
context.Add( |
||||
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout")); |
new TemplateDefinition( |
||||
|
StandardEmailTemplates.Layout, |
||||
|
defaultCultureName: "en", |
||||
|
isLayout: true |
||||
|
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Layout") |
||||
|
); |
||||
|
|
||||
context.Add(new EmailTemplateDefinition(StandardEmailTemplates.SimpleMessage, defaultCultureName: "en") |
context.Add( |
||||
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message")); |
new TemplateDefinition( |
||||
|
StandardEmailTemplates.Message, |
||||
|
defaultCultureName: "en", |
||||
|
layout: StandardEmailTemplates.Layout |
||||
|
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Message") |
||||
|
); |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
@ -1 +0,0 @@ |
|||||
{{message}} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class EmailTemplateContributorList : List<IEmailTemplateContributor> |
|
||||
{ |
|
||||
public string GetOrNull(string cultureName) |
|
||||
{ |
|
||||
foreach (var contributor in this.AsQueryable().Reverse()) |
|
||||
{ |
|
||||
var templateString = contributor.GetOrNull(cultureName); |
|
||||
if (templateString != null) |
|
||||
{ |
|
||||
return templateString; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,38 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using System.Collections.Immutable; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class EmailTemplateDefinitionContext : IEmailTemplateDefinitionContext |
|
||||
{ |
|
||||
protected Dictionary<string, EmailTemplateDefinition> EmailTemplates { get; } |
|
||||
|
|
||||
public EmailTemplateDefinitionContext(Dictionary<string, EmailTemplateDefinition> emailTemplates) |
|
||||
{ |
|
||||
EmailTemplates = emailTemplates; |
|
||||
} |
|
||||
|
|
||||
public virtual EmailTemplateDefinition GetOrNull(string name) |
|
||||
{ |
|
||||
return EmailTemplates.GetOrDefault(name); |
|
||||
} |
|
||||
|
|
||||
public virtual IReadOnlyList<EmailTemplateDefinition> GetAll() |
|
||||
{ |
|
||||
return EmailTemplates.Values.ToImmutableList(); |
|
||||
} |
|
||||
|
|
||||
public virtual void Add(params EmailTemplateDefinition[] definitions) |
|
||||
{ |
|
||||
if (definitions.IsNullOrEmpty()) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
foreach (var definition in definitions) |
|
||||
{ |
|
||||
EmailTemplates[definition.Name] = definition; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class EmailTemplateDefinitionDictionary : Dictionary<string, EmailTemplateDefinition> |
|
||||
{ |
|
||||
public EmailTemplateDefinitionDictionary Add(EmailTemplateDefinition emailTemplateDefinition) |
|
||||
{ |
|
||||
if (ContainsKey(emailTemplateDefinition.Name)) |
|
||||
{ |
|
||||
throw new AbpException( |
|
||||
"There is already an email template definition with given name: " + |
|
||||
emailTemplateDefinition.Name |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
this[emailTemplateDefinition.Name] = emailTemplateDefinition; |
|
||||
|
|
||||
return this; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,74 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Collections.Immutable; |
|
||||
using System.Linq; |
|
||||
using Microsoft.Extensions.DependencyInjection; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class EmailTemplateDefinitionManager : IEmailTemplateDefinitionManager, ISingletonDependency |
|
||||
{ |
|
||||
protected Lazy<IDictionary<string, EmailTemplateDefinition>> EmailTemplateDefinitions { get; } |
|
||||
|
|
||||
protected AbpEmailTemplateOptions Options { get; } |
|
||||
|
|
||||
protected IServiceProvider ServiceProvider { get; } |
|
||||
|
|
||||
public EmailTemplateDefinitionManager( |
|
||||
IOptions<AbpEmailTemplateOptions> options, |
|
||||
IServiceProvider serviceProvider) |
|
||||
{ |
|
||||
ServiceProvider = serviceProvider; |
|
||||
Options = options.Value; |
|
||||
|
|
||||
EmailTemplateDefinitions = |
|
||||
new Lazy<IDictionary<string, EmailTemplateDefinition>>(CreateEmailTemplateDefinitions, true); |
|
||||
} |
|
||||
|
|
||||
public virtual EmailTemplateDefinition Get(string name) |
|
||||
{ |
|
||||
Check.NotNull(name, nameof(name)); |
|
||||
|
|
||||
var template = GetOrNull(name); |
|
||||
|
|
||||
if (template == null) |
|
||||
{ |
|
||||
throw new AbpException("Undefined template: " + name); |
|
||||
} |
|
||||
|
|
||||
return template; |
|
||||
} |
|
||||
|
|
||||
public virtual IReadOnlyList<EmailTemplateDefinition> GetAll() |
|
||||
{ |
|
||||
return EmailTemplateDefinitions.Value.Values.ToImmutableList(); |
|
||||
} |
|
||||
|
|
||||
public virtual EmailTemplateDefinition GetOrNull(string name) |
|
||||
{ |
|
||||
return EmailTemplateDefinitions.Value.GetOrDefault(name); |
|
||||
} |
|
||||
|
|
||||
protected virtual IDictionary<string, EmailTemplateDefinition> CreateEmailTemplateDefinitions() |
|
||||
{ |
|
||||
var templates = new Dictionary<string, EmailTemplateDefinition>(); |
|
||||
|
|
||||
using (var scope = ServiceProvider.CreateScope()) |
|
||||
{ |
|
||||
var providers = Options |
|
||||
.DefinitionProviders |
|
||||
.Select(p => scope.ServiceProvider.GetRequiredService(p) as IEmailTemplateDefinitionProvider) |
|
||||
.ToList(); |
|
||||
|
|
||||
foreach (var provider in providers) |
|
||||
{ |
|
||||
provider.Define(new EmailTemplateDefinitionContext(templates)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return templates; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,121 +0,0 @@ |
|||||
using System; |
|
||||
using System.Globalization; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.Localization; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Volo.Abp.DependencyInjection; |
|
||||
using Volo.Abp.Localization; |
|
||||
|
|
||||
namespace Volo.Abp.Emailing.Templates |
|
||||
{ |
|
||||
public class EmailTemplateProvider : IEmailTemplateProvider, ITransientDependency |
|
||||
{ |
|
||||
protected IEmailTemplateDefinitionManager EmailTemplateDefinitionManager; |
|
||||
protected ITemplateLocalizer TemplateLocalizer { get; } |
|
||||
protected AbpEmailTemplateOptions Options { get; } |
|
||||
protected IStringLocalizerFactory StringLocalizerFactory; |
|
||||
|
|
||||
public EmailTemplateProvider(IEmailTemplateDefinitionManager emailTemplateDefinitionManager, |
|
||||
ITemplateLocalizer templateLocalizer, IStringLocalizerFactory stringLocalizerFactory, |
|
||||
IOptions<AbpEmailTemplateOptions> options) |
|
||||
{ |
|
||||
EmailTemplateDefinitionManager = emailTemplateDefinitionManager; |
|
||||
TemplateLocalizer = templateLocalizer; |
|
||||
StringLocalizerFactory = stringLocalizerFactory; |
|
||||
Options = options.Value; |
|
||||
} |
|
||||
|
|
||||
public async Task<EmailTemplate> GetAsync(string name) |
|
||||
{ |
|
||||
return await GetAsync(name, CultureInfo.CurrentUICulture.Name); |
|
||||
} |
|
||||
|
|
||||
public async Task<EmailTemplate> GetAsync(string name, string cultureName) |
|
||||
{ |
|
||||
return await GetInternalAsync(name, cultureName); |
|
||||
} |
|
||||
|
|
||||
protected virtual async Task<EmailTemplate> GetInternalAsync(string name, string cultureName) |
|
||||
{ |
|
||||
var emailTemplateDefinition = EmailTemplateDefinitionManager.GetOrNull(name); |
|
||||
if (emailTemplateDefinition == null) |
|
||||
{ |
|
||||
// TODO: Localized message
|
|
||||
throw new AbpException($"email template {name} not definition"); |
|
||||
} |
|
||||
|
|
||||
var emailTemplateString = emailTemplateDefinition.Contributors.GetOrNull(cultureName); |
|
||||
if (emailTemplateString == null && emailTemplateDefinition.DefaultCultureName != null) |
|
||||
{ |
|
||||
emailTemplateString = |
|
||||
emailTemplateDefinition.Contributors.GetOrNull(emailTemplateDefinition.DefaultCultureName); |
|
||||
if (emailTemplateString != null) |
|
||||
{ |
|
||||
cultureName = emailTemplateDefinition.DefaultCultureName; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (emailTemplateString != null) |
|
||||
{ |
|
||||
var emailTemplate = new EmailTemplate(emailTemplateString, emailTemplateDefinition); |
|
||||
|
|
||||
await SetLayoutAsync(emailTemplateDefinition, emailTemplate, cultureName); |
|
||||
|
|
||||
if (emailTemplateDefinition.SingleTemplateFile) |
|
||||
{ |
|
||||
await LocalizeAsync(emailTemplateDefinition, emailTemplate, cultureName); |
|
||||
} |
|
||||
|
|
||||
return emailTemplate; |
|
||||
} |
|
||||
|
|
||||
// TODO: Localized message
|
|
||||
throw new AbpException($"{cultureName} template not exist!"); |
|
||||
} |
|
||||
|
|
||||
protected virtual async Task SetLayoutAsync(EmailTemplateDefinition emailTemplateDefinition, |
|
||||
EmailTemplate emailTemplate, string cultureName) |
|
||||
{ |
|
||||
var layout = emailTemplateDefinition.Layout; |
|
||||
if (layout.IsNullOrWhiteSpace()) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (layout == EmailTemplateDefinition.DefaultLayoutPlaceHolder) |
|
||||
{ |
|
||||
layout = Options.DefaultLayout; |
|
||||
} |
|
||||
|
|
||||
var layoutTemplate = await GetInternalAsync(layout, cultureName); |
|
||||
|
|
||||
emailTemplate.SetLayout(layoutTemplate); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task LocalizeAsync(EmailTemplateDefinition emailTemplateDefinition, |
|
||||
EmailTemplate emailTemplate, string cultureName) |
|
||||
{ |
|
||||
if (emailTemplateDefinition.LocalizationResource == null) |
|
||||
{ |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
var localizer = StringLocalizerFactory.Create(emailTemplateDefinition.LocalizationResource); |
|
||||
if (cultureName != null) |
|
||||
{ |
|
||||
using (CultureHelper.Use(new CultureInfo(cultureName))) |
|
||||
{ |
|
||||
emailTemplate.SetContent(TemplateLocalizer.Localize(localizer, emailTemplate.Content)); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
emailTemplate.SetContent( |
|
||||
TemplateLocalizer.Localize(localizer, emailTemplate.Content) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||