## 4.1 ABP应用层 - 应用服务
应用服务用于将领域(业务)逻辑暴露给展现层。展现层通过传入DTO(数据传输对象)参数来调用应用服务,而应用服务通过领域对象来执行相应的业务逻辑并且将DTO返回给展现层。因此,展现层和领域层将被完全隔离开来。在一个理想的层级项目中,展现层应该从不直接访问领域对象。
### 4.1.1 IApplicationService接口
在ABP中,一个应用服务需要实现 **IApplicationService** 接口。最好的实践是针对每个应用服务都创建相应的接口。所以,我们首先定义一个应用服务接口,如下所示:
```csharp
public interface IPersonAppService : IApplicationService
{
void CreatePerson(CreatePersonInput input);
}
```
**IPersonAppService** 只有一个方法,它将被展现层调用来创建一个新的Person。**CreatePersonInput** 是一个DTO对象,如下所示:
```csharp
public class CreatePersonInput
{
[Required]
public string Name { get; set; }
public string EmailAddress { get; set; }
}
```
接着,我们实现IPersonAppService接口:
```csharp
public class PersonAppService : IPersonAppService
{
private readonly IRepository<Person> _personRepository;
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = _personRepository.FirstOrDefault(p => p.EmailAddress == input.EmailAddress);
if (person != null)
{
throw new UserFriendlyException("There is already a person with given email address");
}
person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
}
}
```
以下是几个重要提示:
* PersonAppService通过IRepository\<Person\>来执行数据库操作。它通过构造器注入模式来生成。我们在这里使用了依赖注入。
* PersonAppService实现了 **IApplicationService** (通过IPersonAppService继承IApplicationService)。ABP会自动地把它注册到依赖注入系统中,并可以注入到别的类型中使用。
* **CreatePerson** 方法需要一个 **CreatePersonInput** 类型的参数。这是一个作为输入的DTO,它将被ABP自动验证其数据有效性。可以查看DTO和数据有效性验证(Validation)文档获取相关细节。
### 4.1.2 应用服务类
应用服务(Application Services)需要实现IApplicationService接口。当然,你可以选择将你的应用服务(Application Services)继承自 **ApplicationService** 基类,这样你的应用服务也就自然而然的实现 **IApplicationService** 接口了。ApplicationService基类提供了方便的日志记录和本地化功能。在此建议你针对你的应用程序创建一个应用服务基类继承自ApplicationService类型。这样你就可以添加一些公共的功能来提供给你的所有应用服务使用。一个应用服务示例如下所示:
```csharp
public class TaskAppService : ApplicationService, ITaskAppService
{
public TaskAppService()
{
LocalizationSourceName = "SimpleTaskSystem";
}
public void CreateTask(CreateTaskInput input)
{
//记录日志,Logger定义在ApplicationService中
Logger.Info("Creating a new task with description: " + input.Description);
//获取本地化文本(L是LocalizationHelper.GetString(...)的简便版本, 定义在 ApplicationService类型)
var text = L("SampleLocalizableTextKey");
//TODO: Add new task to database...
}
}
```
本例中我们在构造函数中定义了 **LocalizationSourceName**,但你可以在基类中定义它,这样你就不需要在每个具体的应用服务中定义它。查看日志记录(logging)和本地化(localization)文档可以获取更多的相关信息。
### 4.1.3 CrudAppService 和 AsyncCrudAppService
如果你想为某个特定的实体创建基于CRUD的应用服务,该服务具有这些方法:**Create,Delete,Get,GetAll**;那么(为了方便快捷),我们可以继承 **CrudAppService**(或者 **AsyncCrudAppService** 如果你想要创建async方法)类来简单实现这些功能。**CrudAppService** 是一个可扩展的泛型基类,该类的泛型参数是对指定实体操作与之相关的 **Entity和DTO** 类,并且你可以根据你的需要自定义覆盖基类的功能。
#### 关于CRUD的简单应用服务示例
假设我们有一个任务实体的定义,如下所示:
```csharp
public class Task : Entity, IHasCreationTime
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationTime { get; set; }
public TaskState State { get; set; }
public Person AssignedPerson { get; set; }
public Guid? AssignedPersonId { get; set; }
public Task()
{
CreationTime = Clock.Now;
State = TaskState.Open;
}
}
```
然后,我们为该实体创建一个DTO:
```csharp
[AutoMap(typeof(Task))]
public class TaskDto : EntityDto, IHasCreationTime
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime CreationTime { get; set; }
public TaskState State { get; set; }
public Guid? AssignedPersonId { get; set; }
public string AssignedPersonName { get; set; }
}
```
使用AutoMap特性,自动创建实体和DTO的映射配置。那么,我们可以创建一个应用服务,如下所示:
```csharp
public class TaskAppService : AsyncCrudAppService<Task, TaskDto>
{
public TaskAppService(IRepository<Task> repository)
: base(repository)
{
}
}
```
我们注入了仓储并且传递该参数给基类(如果我们想要创建 **sync** 方法来替代 **async** 方法,那么我们可以继承自 **CrudAppService**)。。好了,现在 **TaskAppService** 就有了简单的CRUD方法了。如果你想为你的应用服务定义一个接口,那么如下所示:
```csharp
public interface ITaskAppService : IAsyncCrudAppService<TaskDto>
{
}
```
注意:**IAsyncCrudAppService** 接口不会取得实体(Task)作为泛型参数。因为,实体与之相关的实现不应该包括在公共接口里面。现在,我们可以在 **TaskAppService** 类中实现 **ITaskAppService** 接口。如下所示:
```csharp
public class TaskAppService : AsyncCrudAppService<Task, TaskDto>, ITaskAppService
{
public TaskAppService(IRepository<Task> repository)
: base(repository)
{
}
}
```
#### 为应用服务自定义CRUD
##### Getting List
CRUD 应用服务使用 **PagedAndSortedResultRequestInput** 作为方法 **GetAll** 的默认参数,该参数提供了可选的排序和分页参数。但是你可能想添加其他参数到GetAll方法。例如,你可能想添加某些自定义的过滤。这样的话,你可以为GetAll方法创建一个DTO。如下所示:
```csharp
public class GetAllTasksInput : PagedAndSortedResultRequestInput
{
public TaskState? State { get; set; }
}
```
我们继承 **PagedAndSortedResultRequestInput** 类(当然这不是必须的,如果你想要使用基类的分页和排序参数的话)并且添加一个可选的 **State** 属性,通过State属性来过滤Task。现在,为了能够应用自定义过滤,我们应该改变 **TaskAppService**。如下所示:
```csharp
public class TaskAppService : AsyncCrudAppService<Task, TaskDto, int, GetAllTasksInput>
{
public TaskAppService(IRepository<Task> repository)
: base(repository)
{
}
protected override IQueryable<Task> CreateFilteredQuery(GetAllTasksInput input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.State.HasValue, t => t.State == input.State.Value);
}
}
```
首先,我们添加了 **GetAllTasksInput** 作为第4个泛型参数到AsyncCrudAppService类(第三个参数是实体的主键)。然后,重写 **CreateFilteredQuery** 方法来应用自定义过滤。这个方法是作为 **AsyncCrudAppService** 类的一个扩展点(为了简化条件过滤,我们使用了ABP的一个扩展方法WhereIf,但实际上我们所做的就是对IQueryable的过滤)。
注意:如果你已经创建应用服务的接口,那么你也需要为该接口添加泛型参数。
##### Create和Update
注意:我们使用相同的DTO(TaskDto)去取得,创建以及更新任务;但是在实际的应用中,这样使用可能不太好。所以,我们需要 **自定义创建和更新DTOs**。首先,我们创建一个 **CreateTaskInput** 类:
```csharp
[AutoMapTo(typeof(Task))]
public class CreateTaskInput
{
[Required]
[MaxLength(Task.MaxTitleLength)]
public string Title { get; set; }
[MaxLength(Task.MaxDescriptionLength)]
public string Description { get; set; }
public Guid? AssignedPersonId { get; set; }
}
```
然后创建一个 **UpdateTaskInput** DTO:
```csharp
[AutoMapTo(typeof(Task))]
public class UpdateTaskInput : CreateTaskInput, IEntityDto
{
public int Id { get; set; }
public TaskState State { get; set; }
}
```
对于更新操作,我想要继承 **CreateTaskInput** 类,这样会包括该类的所有属性。在这里扩展 **IEntity** (或者对于其他类型的主键可以扩展IEntity\<PrimaryKey\>接口) 是必须(**Required**)的;因为,我们需要知道那个实体需要更新。最后,我还添加了一个不在CreateTaskInput类中的属性 **State**。
现在,我们可以使用这些DTO类来作为 **AsyncCrudAppService** 类的泛型参数,如下所示:
```csharp
public class TaskAppService : AsyncCrudAppService<Task, TaskDto, int, GetAllTasksInput, CreateTaskInput, UpdateTaskInput>
{
public TaskAppService(IRepository<Task> repository)
: base(repository)
{
}
protected override IQueryable<Task> CreateFilteredQuery(GetAllTasksInput input)
{
return base.CreateFilteredQuery(input)
.WhereIf(input.State.HasValue, t => t.State == input.State.Value);
}
}
```
我们不需要添加额外的代码来做出更改。
#### 其他
如果你想要为 **Get和Delete** 方法自定义input DTOs 参数,那么就要为AsyncCrudAppService类能设置更多的泛型参数。那么,所有的基类方法都是virtual,这样你就可以重写或者自定义这些行为。
### 4.1.4 工作单元
在ABP中,一个应用服务方法默认是一个工作单元。
**(1)连接 & 事务管理 (For connection & transaction management)**
在应用服务方法中,如果我们需要调用两个仓储方法,那么这些方法必须为一个事务。举个例子:
```csharp
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
```
我们向Person表插入一个数据,接着在其他表中修改了Person计数字段的值。这两个操作实现于不同的仓储中,但是它们使用了相同的数据连接和事务。这是怎么实现的呢?
对于UOW模式,当事务启动并且开始执行CreatePerson方法的时候,ABP会自动地打开数据库。在方法结束时,如果未发生异常该事务将会被提交,并确保关闭数据库连接。因此,CreatePerson方法中的所有数据库操作将作为一个事务(具有原子性),当有异常抛出时这些事务中的操作将会回滚。所以,示例中的两个仓储方法使用了相同的数据连接和事务。
当你调用仓储中的GetAll()方法时,它将返回一个IQueryable\<T\>。数据库连接应会在调用仓储方法后打开。这是因为IQueryable\<T\>和LINQ的延迟执行。当你调用类似ToList()方法时,数据库查询才会真正的开始执行。来看下面的示例:
```csharp
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
//获取 IQueryable<Person>
var query = _personRepository.GetAll();
//过滤数据
if (!string.IsNullOrEmpty(input.SearchedName))
{
query = query.Where(person => person.Name.StartsWith(input.SearchedName));
}
if (input.IsActive.HasValue)
{
query = query.Where(person => person.IsActive == input.IsActive.Value);
}
//获取分页
var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
return new SearchPeopleOutput {People = Mapper.Map<List<PersonDto>>(people)};
}
```
由于一个应用服务(Application Services)方法就是一个工作单元,所以数据库连接在方法执行期间都是开启的。如果你在非应用服务(Application Services)中调用GetAll(),你需要显式的使用工作单元模式。如:在Controller的Action方法中要使用GetAll()或调用多个有对数据库操作的AppService方法时, 应该将Action方法使用virtual修饰,并在Action的上面通过[UnitOfWork]进行显示开启工作单元模式。
注意我使用了AutoMapper库将List\<Person\>转换成List\<PersonDto\>。可以查看DTO文档获取相关细节。
>译者-天道注:这里要说一下,就是uow和非uow模式的区别,两种模式对于数据库连接的打开和关闭是不同的。对于控制器的方法,ABP默认是非 uow模式,此时如果调用方法会报错,提示数据库未连接。解决的办法是在方法加上virtual。
**(2)自动保存数据修改 (For automatically saving changes)**
对于工作单元方法(应用服务(Application Services)方法),在方法结束时ABP将会自动保存所有数据修改。假设我们需要一个应用服务(Application Services)方法来更新一个Person的Name:
```csharp
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
```
就是这样,Name被成功修改!我们甚至不需要调用_personRepository.Update方法。ORM框架在工作单元中会跟踪所有实体修改并将修改更新到数据库中。
### 4.1.4 应用服务的生命周期 ###
所有应用服务(Application Services)实例的生命周期都是暂时的(Transient)。这意味着在每次使用都会创建新的应用服务(Application Services)实例。ABP坚决地使用依赖注入技术。当一个应用服务(Application Services)类型需要被注入时,该应用服务(Application Services)类型的新实例将会被依赖注入容器自动创建。查看依赖注入(Dependency Injection)文档获取更多信息。
- 文章&教程
- Abp
- 1.1ABP总体介绍-入门介绍
- 1.2ABP总体介绍-多层架构体系
- 1.3ABP总体介绍-模块系统
- 1.4ABP总体介绍-启动配置
- 1.5ABP总体介绍-多租户
- 1.6ABP总体介绍-集成OWIN
- 2.1ABP公共结构-依赖注入
- 2.2ABP公共结构-会话管理
- 2.3ABP公共结构-缓存管理
- 2.4ABP公共结构-日志管理
- 2.5ABP公共结构-设置管理
- 2.6ABP公共结构-时区设置
- 3.1ABP领域层-实体
- 3.2ABP领域层-值对象
- 3.3ABP领域层-仓储
- 3.4ABP领域层-领域服务
- 3.5ABP领域层-工作单元
- 3.6ABP领域层-领域事件
- 3.7ABP领域层-数据过滤器
- 4.1ABP应用层-应用服务
- 4.2ABP应用层-数据传输对象
- 4.3ABP应用层-数据传输对象验证
- 4.4ABP应用层-权限认证
- 4.5ABP应用层-功能管理
- 4.6ABP应用层-审计日志
- 5.1ABP分布式服务-ASP.NET WebApi
- 5.2ABP分布式服务-动态WebApi层
- 5.3ABP分布式服务-集成OData
- 5.4ABP分布式服务-集成SwaggerUI
- 6.1ABP表现层-Mvc控制器
- 6.2ABP表现层-Mvc视图
- 6.3ABP表现层-本地化
- 6.4ABP表现层-导航栏
- 6.5ABP表现层-异常处理
- 6.6.2-AJAX
- 6.6.3-Notification
- 6.6.4-Message
- 6.6.5-UIBlockBusy
- 6.6.6-EventBus
- 6.6.7-Logging
- 6.6.8-OtherUtilities
- 6.6ABP表现层-JavascriptAPI
- 6.7ABP表现层-嵌入资源文件
- 6.8ASP.NET-Core
- 6.9CSRF和XSRF保护
- 7.1ABP后台服务-后台作业和工人
- 7.2ABP后台服务-集成Hangfire
- 8.1ABP实时服务-通知系统
- 8.2ABP实时服务-集成SignalR
- 9.1ABP基础设施层-集成Entity Framework
- 9.2ABP基础设施层-集成NHibernate
- 9.3ABP基础设施层-集成Entity Framework Core
- abp合并版160929
- md文档排版规范
- AbpZero
- 1.1ABPZero-概述
- 1.2ABPZero-安装
- 1.3ABPZero-启动模板
- 1.4ABPZero-启动模板Core
- 2.1ABPZero-多租户管理
- 2.2ABPZero-版本管理
- 2.4ABPZero-组织单位管理