ABP框架系列之十:(Application-Services-應用服務)

weixin_34409357發表於2017-11-28

Application Services are used to expose domain logic to the presentation layer. An Application Service is called from presentation layer with a DTO (Data Transfer Object) as parameter, uses domain objects to perform some specific business logic and returns a DTO back to the presentation layer. Thus, Presentation layer is completely isolated from Domain layer. In an ideally layered application, presentation layer never directly works with domain objects.

應用程式服務用於將域邏輯暴露到表示層中。應用服務是從表現層與DTO(資料傳輸物件)為引數,使用域物件執行一些具體的業務邏輯,並返回一個DTO返回給表現層。因此,表示層與域層完全隔離。在理想的分層應用程式中,表示層從不直接使用域物件。

IApplicationService Interface

In ASP.NET Boilerplate, an application service should implement IApplicationService interface. It's good to create an interface for each Application Service. So, we first define an interface for an application service as shown below:

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

IPersonAppService has only one method. It's used by presentation layer to create a new person. CreatePersonInput is a DTO object as shown below:

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

    public string EmailAddress { get; set; }
}

Then we can implement the 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);
    }
}

There are some important points here:

  • PersonAppService uses IRepository<Person> to perform database operations. It uses constructor injection pattern. We're using dependency injection here.
  • PersonAppService implements IApplicationService (since IPersonAppService extends IApplicationService), it's automatically registered to Dependency Injection system by ASP.NET Boilerplate and can be injected and used by other classes. Naming convention is important here.(命名約定在這裡很重要) See dependency injection document for more.
  • CreatePerson method gets CreatePersonInput. It's an input DTO and automatically validated by ASP.NET Boilerplate. See DTO and validation documents for details.

ApplicationService Class

An application service should implement IApplicationService interface as declared above. Also, optionally, can be derived from ApplicationService base class. Thus, IApplicationService is inherently implemented. Also, ApplicationService class has some base functionality that makes easy to logging, localization and so on... It's suggested to create a special base class for your application services that extends ApplicationService class. Thus, you can add some common functionality for all your application services. A sample application service class is shown below:

應用服務應該實現iapplicationservice介面以上的宣告。另外,還可以實現應用服務的基類。因此,iapplicationservice本質上是實現。同時,應用服務類的一些基本功能,使得日誌,本地化等等…建議建立您的應用程式服務,擴充套件應用服務類特殊的基類。因此,您可以為所有應用程式服務新增一些公共功能。下面顯示了一個示例應用程式服務類:

public class TaskAppService : ApplicationService, ITaskAppService
{
    public TaskAppService()
    {
        LocalizationSourceName = "SimpleTaskSystem";
    }

    public void CreateTask(CreateTaskInput input)
    {
        //Write some logs (Logger is defined in ApplicationService class)
        Logger.Info("Creating a new task with description: " + input.Description);

        //Get a localized text (L is a shortcut for LocalizationHelper.GetString(...), defined in ApplicationService class)
        var text = L("SampleLocalizableTextKey");

        //TODO: Add new task to database...
    }
}

You can have a base class which defines LocalizationSourceName in it's constructor. Thus, you do not repeat it for all service classes. See logging and localization documents for more informations on this topics.

你可以在它的建構函式有一個基類定義了localizationsourcename。因此,不要為所有服務類重複它。有關此主題的更多資訊,請參見日誌和本地化文件。

CrudAppService and AsyncCrudAppService Classes

If you need to create an application service that will have Create, Update, Delete, Get, GetAll methods for a specific entity, you can inherit from CrudAppService (or AsyncCrudAppService if you want to create async methods) class to create it easier. CrudAppService base class is generic which gets related Entity and DTO types as generic arguments and is extensible which allows you to override functionality when you need to customize it.

如果你需要建立一個應用程式服務,將建立,更新,刪除,得到的,對於一個特定的實體獲得的方法,你可以從crudappservice(或asynccrudappservice如果你想建立非同步方法)的類來建立更容易。crudappservice基類是通用的,得到了相關的實體和DTO型別作為泛型引數是可擴充套件的,允許你覆蓋的功能時,你需要定製。

Simple CRUD Application Service Example

Assume that we have a Task entity defined below:

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;
    }
}

And we created a DTO for this entity:

[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 attribute creates auto mapping configuration between entity and dto. Now, we can create an application service as shown below:

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

    }
}

We injected the repository and passed it to the base class (We could inherit from CrudAppService if we want to create sync methods instead of async methods). That's all! TaskAppService now have simple CRUD methods. If you want to define an interface for the application service, you can create your interface as shown below:

我們注入倉庫交給基類(我們可以從crudappservicej繼承如果我們想建立同步方法代替非同步方法)。這就是全部!taskappservice現在有簡單的CRUD方法。如果您想為應用程式服務定義一個介面,您可以建立如下所示的介面:

public interface ITaskAppService : IAsyncCrudAppService<TaskDto>
{
        
}

Notice that IAsyncCrudAppService does not get the entity (Task) as generic argument. Because, entity is related to implementation and should not be included in public interface. Now, we can implement ITaskAppService interface for the TaskAppService class:

注意,iasynccrudappservice沒有實體(任務)作為泛型引數。因為實體與實現有關,不應該包含在公共介面中。現在,我們可以實現對taskappservice類itaskappservice介面:

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

    }
}

Customize CRUD Application Services(定製的CRUD應用服務

Getting List

Crud application service gets PagedAndSortedResultRequestDto as argument for GetAll method as default, which provides optional sorting and paging parameters. But you may want to add another parameters for GetAll method. For example, you may want to add some custom filters. In that case, you can create a DTO for GetAll method. Example:

CRUD應用服務得到PagedAndSortedResultRequestDto為getAll方法作為預設引數,它提供了可選的排序和分頁引數。但你可能要新增另一個引數的獲得方法。例如,您可能需要新增一些自定義過濾器。在這種情況下,你可以建立一個DTO為getAll方法。例子:

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

We inherit from PagedAndSortedResultRequestInput (which is not required, but wanted to use paging & sorting parameters form the base class) and added an optional State property to filter tasks by state. Now, we should change TaskAppService in order to apply the custom filter:

我們從PagedAndSortedResultRequestInput(這是不需要的,但要使用分頁和排序的引數形式的基類),新增一個可選的狀態屬性的狀態濾波任務。現在,我們應該改變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);
    }
}

First, we added GetAllTasksInput as 4th generic parameter to AsyncCrudAppService class (3rd one is PK type of the entity). Then overrided CreateFilteredQuery method to apply custom filters. This method is an extension point for AsyncCrudAppService class (WhereIf is an extension method of ABP to simplify conditional filtering. But actually what we do is to simply filter an IQueryable).

首先,我們增加了GetAllTasksInput為第四個泛型引數asynccrudappservice類(第三個是PK型的實體)。然後超越createfilteredquery方法應用自定義過濾器。該方法是asynccrudappservice類擴充套件點(如果是一個擴充套件方法和簡化條件過濾。但實際上我們做的是簡單的過濾一個IQueryable)。

Note that: If you have created application service interface, you need to add same generic arguments to that interface too.

注意:如果您已經建立了應用程式服務介面,那麼您也需要向該介面新增相同的泛型引數。

Create and Update

Notice that we are using same DTO (TaskDto) for getting, creating and updating tasks which may not be good for real life applications. So, we may want to customize create and update DTOs. Let's start by creating aCreateTaskInput class:

注意,我們使用的是同樣的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; }
}

And create an UpdateTaskInput DTO:

[AutoMapTo(typeof(Task))]
public class UpdateTaskInput : CreateTaskInput, IEntityDto
{
    public int Id { get; set; }

    public TaskState State { get; set; }
}

I wanted to inherit from CreateTaskInput to include all properties for Update operation (but you may want different). Implementing IEntity (or IEntity<PrimaryKey> for different PK than int) is required here, because we need to know which entity is being updated. Lastly, I added an additional property, State, which is not in CreateTaskInput.

我想從createtaskinput包括更新操作的所有屬性(但你可能需要不同的)。實現認同(or IEntity<PrimaryKey> for different PK than int)是必須的,因為我們需要知道哪些實體正在更新。最後,我新增了一個額外的屬性,狀態,這是不是在CreateTaskInput。

Now, we can use these DTO classes as generic arguments for AsyncCrudAppService class, as shown below:

現在,我們可以使用這些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);
    }
}

No need to an additional code change.

不需要額外的程式碼更改。

Other Method Arguments(其他方法的引數)

AsyncCrudAppService can get more generic arguments if you want to customize input DTOs for Get and Delete methods. Also, all methods of the base class is virtual, so you can override them to customize the behaviour.

asynccrudappservice可以得到更一般的引數如果你想自定義輸入DTOs和刪除方法。此外,基類的所有方法都是虛擬的,因此您可以重寫它們以自定義行為。

CRUD Permissions(CRUD許可權

You probably need to authorize your CRUD methods. There are pre-defined permission properties you can set: GetPermissionName, GetAllPermissionName, CreatePermissionName, UpdatePermissionName and DeletePermissionName. Base CRUD class automatically checks permissions if you set them. You can set it in the constructor as shown below:

你可能需要授權你的CRUD方法。有預先定義的許可權屬性可以設定:getpermissionname,getallpermissionname,CreatePermissionName,updatepermissionname和deletepermissionname。基礎的CRUD類自動檢查許可權,如果你讓他們。您可以在建構函式中設定它,如下所示:

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

Alternatively, you can override appropriate permission checker methods to manually check permissions: CheckGetPermission(), CheckGetAllPermission(), CheckCreatePermission(), CheckUpdatePermission(), CheckDeletePermission(). As default, they all calls CheckPermission(...) method with related permission name which simply calls IPermissionChecker.Authorize(...) method.

或者,你可以覆蓋相應許可權檢查方法手動檢查許可權:checkgetpermission(),checkgetallpermission(),checkcreatepermission(),checkupdatepermission(),checkdeletepermission()。在預設的情況下,他們的所有呼叫checkPermission(…)與相關許可權名稱簡單地呼叫IPermissionChecker法。授權(…)方法。

Unit of Work

An application service method is a unit of work by default in ASP.NET Boilerplate. Thus, any application service method is transactional and automatically saves all database changes at the end of the method. 

應用服務的方法是通過在ASP.NET樣板預設工作單元。因此,任何應用程式服務方法都是事務性的,並在方法結束時自動儲存所有資料庫更改。

See unit of work documentation for more.

Lifetime of an Application Service

All application service instances are Transient. It means, they are instantiated per usage. See Dependency Injection documentation for more information.

所有應用程式服務例項都是暫時的。它意味著,每個使用都例項化它們。有關更多資訊,請參見依賴注入文件。

相關文章