深入淺出-應用服務

應用服務實現應用程序的用例, 將領域層邏輯公開給表示層.

從表示層(可選)調用應用服務,DTO (數據傳對象) 作爲參數. 返回(可選)DTO給表示層.

示例

圖書實體

假設你有一個Book實體(聚合根), 如下所示:

public class Book : AggregateRoot<Guid>
{
    public const int MaxNameLength = 128;

    public virtual string Name { get; protected set; }

    public virtual BookType Type { get; set; }

    public virtual float? Price { get; set; }

    protected Book()
    {

    }

    public Book(Guid id, [NotNull] string name, BookType type, float? price = 0)
    {
        Id = id;
        Name = CheckName(name);
        Type = type;
        Price = price;
    }

    public virtual void ChangeName([NotNull] string name)
    {
        Name = CheckName(name);
    }

    private static string CheckName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException($"name can not be empty or white space!");
        }

        if (name.Length > MaxNameLength)
        {
            throw new ArgumentException($"name can not be longer than {MaxNameLength} chars!");
        }

        return name;
    }
}
  • Book實體中定義MaxNameLength限制Name屬性的最大長度。
  • Book構造函數與ChangeName確保Name屬性值的有效性. 請注意, Name的setter不是public。

ABP不會強制開發者這樣設計實體, 可以將所有的屬性設置Public set/get. 由你來決定是否全面實施DDD.

IBookAppService接口

在ABP中應用程序服務應該實現IApplicationService接口. 推薦每個應用程序服務創建一個接口:

public interface IBookAppService : IApplicationService
{
    Task CreateAsync(CreateBookDto input);
}

我們將實現Create方法作爲示例. CreateBookDto定義如下:

public class CreateBookDto
{
    [Required]
    [StringLength(Book.MaxNameLength)]
    public string Name { get; set; }

    public BookType Type { get; set; }

    public float? Price { get; set; }
}

有關DTO更的教程,請參見數據傳輸對象文檔

BookAppService(實現)

public class BookAppService : ApplicationService, IBookAppService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public BookAppService(IRepository<Book, Guid> bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public async Task CreateAsync(CreateBookDto input)
    {
        var book = new Book(
            GuidGenerator.Create(),
            input.Name,
            input.Type,
            input.Price
        );

        await _bookRepository.InsertAsync(book);
    }
}
  • BookAppService繼承了基類ApplicationService· 這不是必需的, 但是ApplicationService提供了應用服務常見的需求(比如本示例服務中使用的GuidGenerator). 如果不繼承它, 我們需要在服務中手動注入IGuidGenerator(參見Guid生成文檔)。
  • BookAppService按照預期實現了IBookAppService。
  • BookAppService 注入了 IRepository<Book, Guid>(請參見倉儲)在CreateAsync方法內部使用倉儲將新實體插入數據庫。
  • CreateAsync使用Book實體的構造函數從給定的Input值創建新的Book對象。

數據傳輸對象

應用服務使用並返回DTO而不是實體. ABP不會強制執行此規則. 但是將實體暴露給表示層(或遠程客戶端)存在重大問題, 所以不建議返回實體。

有關更多信息, 請參見DTO文檔

對象到對象映射

CreateBook方法使用參數CreateBookDto對象手動創建Book實體. 因爲Book實體的構造函數強制執行(我們是這樣設計的)。

但是在很多情況下使用自動對象映射從相似對象設置對象的屬性更加方便實用. ABP提供了一個對象到對象映射基礎設施,使其變得更加容易。

讓我們創建另一種獲取Book的方法. 首先,在IBookAppService接口中定義方法:

public interface IBookAppService : IApplicationService
{
    Task CreateAsync(CreateBookDto input);

    Task<BookDto> GetAsync(Guid id); //New method
}

BookDto是一個簡單的DTO類, 定義如下:

public class BookDto
{
    public Guid Id { get; set; }

    public string Name { get; set; }

    public BookType Type { get; set; }

    public float? Price { get; set; }
}

我們創建一個Automapper的Profile類. 例如:

public class MyProfile : Profile
{
    public MyProfile()
    {
        CreateMap<Book, BookDto>();
    }
}

然後使用AbpAutoMapperOptions註冊配置文件:

[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpAutoMapperOptions>(options =>
        {
            //Add all mappings defined in the assembly of the MyModule class
            options.AddMaps<MyModule>();
        });
    }
}

AddMaps 註冊給定類的程序集中所有的配置類,通常使用模塊類. 它還會註冊 attribute 映射. 更多信息請參考對象到對象映射文檔

然後你可以實現GetAsync方法. 如下所示:

public async Task<BookDto> GetAsync(Guid id)
{
    var book = await _bookRepository.GetAsync(id);
    return book.MapTo<BookDto>();
}

MapTo擴展方法通過複製具有相同命名的所有屬性將Book對象轉換爲BookDto對象。

MapTo的另一種替代方法是使用IObjectMapper服務:

public async Task<BookDto> GetAsync(Guid id)
{
    var book = await _bookRepository.GetAsync(id);
    return ObjectMapper.Map<Book, BookDto>(book);
}

雖然第二種語法編寫起來有點困難,但是如果你編寫單元測試,它會更好地工作. 有關更多信息,請參閱對象到對象映射文檔.

驗證

自動驗證應用服務方法的輸入(如ASP.NET Core 控制器的actions). 你可以使用標準數據註釋屬性或自定義驗證方法來執行驗證. ABP還確保輸入不爲空。

請參閱驗證文檔瞭解更多信息。

授權

可以對應用程序服務方法使用聲明性和命令式授權。

請參閱授權文檔瞭解更多信息。

CRUD應用服務

如果需要創建具有Create,Update,Delete和Get方法的簡單CRUD應用服務,則可以使用ABP的基類輕鬆構建服務. 你可以繼承CrudAppService。

示例:

創建繼承ICrudAppService接口的IBookAppService接口。

public interface IBookAppService : 
    ICrudAppService< //Defines CRUD methods
        BookDto, //Used to show books
        Guid, //Primary key of the book entity
        PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books
        CreateUpdateBookDto, //Used to create a new book
        CreateUpdateBookDto> //Used to update a book
{
}

ICrudAppService 有泛型參數來獲取實體的主鍵類型和CRUD操作的DTO類型(它不獲取實體類型,因爲實體類型未向客戶端公開使用此接口)。

爲應用程序服務創建一個接口是最佳做法,但是ABP框架並不強制你這麼做,你可以跳過接口部分。

ICrudAppService聲明以下方法:

public interface ICrudAppService<
    TEntityDto,
    in TKey,
    in TGetListInput,
    in TCreateInput,
    in TUpdateInput>
    : IApplicationService
    where TEntityDto : IEntityDto<TKey>
{
    Task<TEntityDto> GetAsync(TKey id);

    Task<PagedResultDto<TEntityDto>> GetListAsync(TGetListInput input);

    Task<TEntityDto> CreateAsync(TCreateInput input);

    Task<TEntityDto> UpdateAsync(TKey id, TUpdateInput input);

    Task DeleteAsync(TKey id);
}

示例中使用的DTO類是BookDtoCreateUpdateBookDto:

public class BookDto : AuditedEntityDto<Guid>
{
    public string Name { get; set; }

    public BookType Type { get; set; }

    public float Price { get; set; }
}

public class CreateUpdateBookDto
{
    [Required]
    [StringLength(128)]
    public string Name { get; set; }

    [Required]
    public BookType Type { get; set; } = BookType.Undefined;

    [Required]
    public float Price { get; set; }
}

DTO類的Profile類.

public class MyProfile : Profile
{
    public MyProfile()
    {
        CreateMap<Book, BookDto>();
        CreateMap<CreateUpdateBookDto, Book>();
    }
}
  • CreateUpdateBookDto由創建和更新操作共享,但你也可以使用單獨的DTO類。

最後BookAppService實現非常簡單:

public class BookAppService : 
    CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
                        CreateUpdateBookDto, CreateUpdateBookDto>,
    IBookAppService
{
    public BookAppService(IRepository<Book, Guid> repository) 
        : base(repository)
    {
    }
}

CrudAppService實現了ICrudAppService接口中聲明的所有方法. 然後,你可以添加自己的自定義方法或覆蓋和自定義實現。

CrudAppService 有不同數量泛型參數的版本,你可以選擇適合的使用。

AbstractKeyCrudAppService

CrudAppService 要求你的實體擁有一個Id屬性做爲主鍵. 如果你使用的是複合主鍵,那麼你無法使用它。

AbstractKeyCrudAppService 實現了相同的 ICrudAppService 接口,但它沒有假設你的主鍵。

示例

假設你有實體 District,它的CityId 和 Name 做爲複合主鍵,使用 AbstractKeyCrudAppService 時需要你自己實現 DeleteByIdAsync 和 GetEntityByIdAsync 方法:

public class DistrictAppService
    : AbstractKeyCrudAppService<District, DistrictDto, DistrictKey>
{
    public DistrictAppService(IRepository<District> repository)
        : base(repository)
    {
    }

    protected override async Task DeleteByIdAsync(DistrictKey id)
    {
        await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name);
    }

    protected override async Task<District> GetEntityByIdAsync(DistrictKey id)
    {
        return await AsyncQueryableExecuter.FirstOrDefaultAsync(
            Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
        );
    }
}

這個實現需要你創建一個類做爲複合鍵:

public class DistrictKey
{
    public Guid CityId { get; set; }

    public string Name { get; set; }
}

生命週期

應用服務的生命週期是transient的,它們會自動註冊到依賴注入系統。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章