4.1 ABP应用层 - 应用服务

应用服务用于将领域(业务)逻辑暴露给展现层。展现层通过传入DTO(数据传输对象)参数来调用应用服务,而应用服务通过领域对象来执行相应的业务逻辑并且将DTO返回给展现层。因此,展现层和领域层将被完全隔离开来。在一个理想的层级项目中,展现层应该从不直接访问领域对象。

4.1.1 IApplicationService接口

在ABP中,一个应用服务需要实现 IApplicationService 接口。最好的实践是针对每个应用服务都创建相应的接口。所以,我们首先定义一个应用服务接口,如下所示:

public interface IPersonAppService : IApplicationService
{
    void CreatePerson(CreatePersonInput input);
}

IPersonAppService 只有一个方法,它将被展现层调用来创建一个新的Person。CreatePersonInput 是一个DTO对象,如下所示:

public class CreatePersonInput
{
    [Required]
    public string Name { get; set; }

    public string EmailAddress { get; set; }
}

接着,我们实现IPersonAppService接口:

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类型。这样你就可以添加一些公共的功能来提供给你的所有应用服务使用。一个应用服务示例如下所示:

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的简单应用服务示例

假设我们有一个任务实体的定义,如下所示:

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:

[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的映射配置。那么,我们可以创建一个应用服务,如下所示:

public class TaskAppService : AsyncCrudAppService<Task, TaskDto>
{
    public TaskAppService(IRepository<Task> repository) 
        : base(repository)
    {

    }
}

我们注入了仓储并且传递该参数给基类(如果我们想要创建 sync 方法来替代 async 方法,那么我们可以继承自 CrudAppService)。。好了,现在 TaskAppService 就有了简单的CRUD方法了。如果你想为你的应用服务定义一个接口,那么如下所示:

public interface ITaskAppService : IAsyncCrudAppService<TaskDto>
{
        
}

注意:IAsyncCrudAppService 接口不会取得实体(Task)作为泛型参数。因为,实体与之相关的实现不应该包括在公共接口里面。现在,我们可以在 TaskAppService 类中实现 ITaskAppService 接口。如下所示:

public class TaskAppService : AsyncCrudAppService<Task, TaskDto>, ITaskAppService
{
    public TaskAppService(IRepository<Task> repository) 
        : base(repository)
    {

    }
}

为应用服务自定义CRUD

Getting List

CRUD 应用服务使用 PagedAndSortedResultRequestDto 作为方法 GetAll 的默认参数,该参数提供了可选的排序和分页参数。但是你可能想添加其他参数到GetAll方法。例如,你可能想添加某些自定义的过滤。这样的话,你可以为GetAll方法创建一个DTO。如下所示:

public class GetAllTasksInput : PagedAndSortedResultRequestDto
{
    public TaskState? State { get; set; }
}

我们继承 PagedAndSortedResultRequestInput 类(当然这不是必须的,如果你想要使用基类的分页和排序参数的话)并且添加一个可选的 State 属性,通过State属性来过滤Task。现在,为了能够应用自定义过滤,我们应该改变 TaskAppService。如下所示:

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 类:

[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:

[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 类的泛型参数,如下所示:

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 CRUD 权限

你可能需要授权给CRUD方法。ABP中有一些预定义的权限属性可以设置:GetPermissionNam,GetAllPermissionName,CreatePermissionName,UpdatePermissionName 和 DeletePermissionName。如果设置了它们,那么基于CRUD的类会自动检测权限。你可以在构造函数设置它们,如下所示:


public class TaskAppService : AsyncCrudAppService<Task, TaskDto>
{
    public TaskAppService(IRepository<Task> repository) 
        : base(repository)
    {
        CreatePermissionName = "MyTaskCreationPermission";
    }
}

或者,你可以重写权限检测方法来手动检测权限:CheckGetPermission(), CheckGetAllPermission(),CheckCreatePermission(),CheckUpdatePermission(),CheckDeletePermission()。它们都是用相应的权限名称来调用CheckPermission(...)方法,其实就是调用IPermissionChecker.Authorize(...)方法。

4.1.5 工作单元

在ABP中应用服务中的方法默认是一个工作单元。因此,任意应用服务方法都是事务的并在方法最后自动保存所有更改到数据库中。

了解更多请查询工作单元

4.1.6 应用服务的生命周期

所有应用服务(Application Services)实例的生命周期都是暂时的(Transient)。这意味着在每次使用都会创建新的应用服务(Application Services)实例。ABP坚决地使用依赖注入技术。当一个应用服务(Application Services)类型需要被注入时,该应用服务(Application Services)类型的新实例将会被依赖注入容器自动创建。查看依赖注入(Dependency Injection)文档获取更多信息。