🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 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)文档获取更多信息。