|
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": { |
|||
"GivenTenantIsNotAvailable": "Podany tenant jest niedostępny: {0}", |
|||
"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 |
|||
{ |
|||
public class DefaultEmailTemplateProvider : EmailTemplateDefinitionProvider |
|||
public class DefaultEmailTemplateProvider : TemplateDefinitionProvider |
|||
{ |
|||
public override void Define(IEmailTemplateDefinitionContext context) |
|||
public override void Define(ITemplateDefinitionContext context) |
|||
{ |
|||
context.Add(new EmailTemplateDefinition(StandardEmailTemplates.DefaultLayout, defaultCultureName: "en", isLayout: true, layout: null) |
|||
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Layout")); |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
StandardEmailTemplates.Layout, |
|||
defaultCultureName: "en", |
|||
isLayout: true |
|||
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Layout") |
|||
); |
|||
|
|||
context.Add(new EmailTemplateDefinition(StandardEmailTemplates.SimpleMessage, defaultCultureName: "en") |
|||
.AddTemplateVirtualFiles("/Volo/Abp/Emailing/Templates/DefaultEmailTemplates/Message")); |
|||
context.Add( |
|||
new TemplateDefinition( |
|||
StandardEmailTemplates.Message, |
|||
defaultCultureName: "en", |
|||
layout: StandardEmailTemplates.Layout |
|||
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Message") |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +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; |
|||
} |
|||
} |
|||
} |
|||