深入浅出-应用服务

应用服务实现应用程序的用例,将领域层逻辑公开给表示层。

从表示层(可选)调用应用服务,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的,它们会自动注册到依赖注入系统。

posted @ 2020-06-02 16:53  Jöhan  阅读(233)  评论(0编辑  收藏  举报