项目

应用服务

应用服务用于实现应用程序的 用例。它们用于 将领域逻辑暴露给展示层

应用服务从展示层调用(可选),使用 DTO(数据传输对象 作为参数。它使用领域对象来 执行特定的业务逻辑,并(可选)将 DTO 返回给展示层。这样,展示层就与领域层完全 隔离

示例

Book 实体

假设您有一个如下定义的 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 的 get/set。是否完全实现 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 的属性创建新书。

数据传输对象

应用服务获取并返回 DTO,而不是实体。ABP 不强制此规则。但是,将实体暴露给展示层(或远程客户端)存在重大问题,因此不建议这样做。

更多信息请参阅 DTO 文档

对象到对象映射

上面的 CreateAsync 方法根据给定的 CreateBookDto 对象手动创建 Book 实体,因为 Book 实体强制执行此操作(我们这样设计的)。

然而,在许多情况下,使用 自动对象映射 从相似对象设置对象的属性非常实用。ABP 提供了一个 对象到对象映射 基础设施,使这变得更加容易。

对象到对象映射提供了抽象,默认由 Mapperly 库实现。

让我们创建另一个方法来获取一本书。首先,在 IBookAppService 接口中定义该方法:

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

    Task<BookDto> GetAsync(Guid id); //新方法
}

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

Mapperly 要求创建一个映射类,该类实现 MapperBase<Book, BookDto> 类,并带有 [Mapper] 属性,如下所示:

[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
    public override partial BookDto Map(Book source);

    public override partial void Map(Book source, BookDto destination);
}

然后,如果您的应用程序使用多个映射提供程序,您应该将以下配置添加到模块的 ConfigureServices 方法中,以决定使用哪个映射提供程序:

[DependsOn(typeof(AbpMapperlyModule))]
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddMapperlyObjectMapper<MyModule>();
    }
}

通过此配置,您的模块将使用 Mapperly 作为默认映射提供程序,并且您无需手动注册映射类。

然后,您可以如下实现 GetAsync 方法:

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

更多信息请参阅 对象到对象映射文档

验证

应用服务方法的输入会自动验证(如 ASP.NET Core 控制器操作)。您可以使用标准的数据注解属性或自定义验证方法来执行验证。ABP 还确保输入不为空。

更多信息请参阅 验证文档

授权

可以为应用服务方法使用声明式和命令式授权。

更多信息请参阅 授权文档

CRUD 应用服务

如果您需要创建一个简单的 CRUD 应用服务,其中包含 Create、Update、Delete 和 Get 方法,您可以使用 ABP 的 基类 轻松构建服务。您可以继承自 CrudAppService

示例

创建一个继承自 ICrudAppService 接口的 IBookAppService 接口。

public interface IBookAppService : 
    ICrudAppService< //定义 CRUD 方法
        BookDto, //用于展示书籍
        Guid, //书籍实体的主键
        PagedAndSortedResultRequestDto, //用于获取书籍列表时的分页/排序
        CreateUpdateBookDto, //用于创建新书
        CreateUpdateBookDto> //用于更新书籍
{
}

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

Mapperly 定义映射类如下:

[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
    public override partial BookDto Map(Book source);
    public override partial void Map(Book source, BookDto destination);
}

[Mapper]
public partial class CreateUpdateBookDtoToBookMapper : MapperBase<CreateUpdateBookDto, Book>
{
    public override partial Book Map(CreateUpdateBookDto source);
    public override partial void Map(CreateUpdateBookDto source, Book destination);
}
  • 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 实体,其复合主键为 CityIdName。使用 AbstractKeyCrudAppService 需要您自己实现 DeleteByIdAsyncGetEntityByIdAsync 方法:

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

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

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

此实现要求您创建一个表示复合主键的类:

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

    public string Name { get; set; }
}

授权(针对 CRUD 应用服务)

有两种方式对基类应用服务方法进行授权:

  1. 您可以在服务的构造函数中设置策略属性(xxxPolicyName)。示例:
public class MyPeopleAppService : CrudAppService<Person, PersonDto, Guid>
{
    public MyPeopleAppService(IRepository<Person, Guid> repository) 
        : base(repository)
    {
        GetPolicyName = "...";
        GetListPolicyName = "...";
        CreatePolicyName = "...";
        UpdatePolicyName = "...";
        DeletePolicyName = "...";
    }
}

CreatePolicyNameCreateAsync 方法检查,依此类推... 您应该指定在应用程序中定义的策略(权限)名称。

  1. 您可以在服务中覆盖检查方法(CheckXxxPolicyAsync)。示例:
public class MyPeopleAppService : CrudAppService<Person, PersonDto, Guid>
{
    public MyPeopleAppService(IRepository<Person, Guid> repository) 
        : base(repository)
    {
    }

    protected async override Task CheckDeletePolicyAsync()
    {
        await AuthorizationService.CheckAsync("...");
    }
}

您可以在 CheckDeletePolicyAsync 方法中执行任何逻辑。在未授权的情况下,预期会抛出 AbpAuthorizationException,就像 AuthorizationService.CheckAsync 已经做的那样。

基类属性和方法

CRUD 应用服务基类提供了许多有用的基类方法,您可以覆盖这些方法以根据自己的需求进行定制。

CRUD 方法

这些是基本的 CRUD 方法。您可以覆盖其中任何一个以完全自定义操作。以下是方法的定义:

Task<TGetOutputDto> GetAsync(TKey id);
Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input);
Task<TGetOutputDto> CreateAsync(TCreateInput input);
Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input);
Task DeleteAsync(TKey id);

查询

这些是低级方法,可以控制如何从数据库查询实体。

  • CreateFilteredQuery 可以被覆盖以创建按给定输入过滤的 IQueryable<TEntity>。如果您的 TGetListInput 类包含任何过滤器,覆盖此方法并过滤查询是合适的。默认情况下,它返回(未过滤的)仓储(它已经是 IQueryable<TEntity>)。
  • ApplyPaging 用于对查询进行分页。如果您的 TGetListInput 已经实现了 IPagedResultRequest,则无需覆盖此方法,因为 ABP 会自动理解并执行分页。
  • ApplySorting 用于对查询进行排序(按...排序)。如果您的 TGetListInput 已经实现了 ISortedResultRequest,ABP 会自动对查询进行排序。否则,它将回退到 ApplyDefaultSorting,如果您的实体实现了标准的 IHasCreationTime 接口,它会尝试按创建时间排序。
  • GetEntityByIdAsync 用于按 id 获取实体,默认调用 Repository.GetAsync(id)
  • DeleteByIdAsync 用于按 id 删除实体,默认调用 Repository.DeleteAsync(id)

对象到对象映射

这些方法用于将实体转换为 DTO,反之亦然。默认情况下,它们使用 IObjectMapper

  • MapToGetOutputDtoAsync 用于将实体映射为从 GetAsyncCreateAsyncUpdateAsync 方法返回的 DTO。或者,如果您不需要执行任何异步操作,可以覆盖 MapToGetOutputDto
  • MapToGetListOutputDtosAsync 用于将实体列表映射为从 GetListAsync 方法返回的 DTO 列表。它使用 MapToGetListOutputDtoAsync 来映射列表中的每个实体。您可以根据情况覆盖其中一个。或者,如果您不需要执行任何异步操作,可以覆盖 MapToGetListOutputDto
  • MapToEntityAsync 方法有两个重载:
    • MapToEntityAsync(TCreateInput) 用于从 TCreateInput 创建实体。
    • MapToEntityAsync(TUpdateInput, TEntity) 用于从 TUpdateInput 更新现有实体。

其他

处理流

Stream 对象本身不可序列化。因此,如果直接使用 Stream 作为应用服务的参数或返回值,可能会遇到问题。ABP 提供了一种特殊类型 IRemoteStreamContent,用于在应用服务中获取或返回流。

示例:可用于获取和返回流的应用服务接口

using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Content;

namespace MyProject.Test
{
    public interface ITestAppService : IApplicationService
    {
        Task Upload(Guid id, IRemoteStreamContent streamContent);
        Task<IRemoteStreamContent> Download(Guid id);

        Task CreateFile(CreateFileInput input);
        Task CreateMultipleFile(CreateMultipleFileInput input);
    }

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

        public IRemoteStreamContent Content { get; set; }
    }

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

        public IEnumerable<IRemoteStreamContent> Contents { get; set; }
    }
}

您需要配置 AbpAspNetCoreMvcOptions,将 DTO 类添加到 FormBodyBindingIgnoredTypes,以便在 DTO(数据传输对象)中使用 IRemoteStreamContent

Configure<AbpAspNetCoreMvcOptions>(options =>
{
    options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput));
    options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput));
});

示例:可用于获取和返回流的应用服务实现

using System;
using System.IO;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Content;

namespace MyProject.Test
{
    public class TestAppService : ApplicationService, ITestAppService
    {
        public Task<IRemoteStreamContent> Download(Guid id)
        {
            var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.OpenOrCreate);
            return Task.FromResult(
                (IRemoteStreamContent) new RemoteStreamContent(fs) {
                    ContentType = "application/octet-stream" 
                }
            );
        }

        public async Task Upload(Guid id, IRemoteStreamContent streamContent)
        {
            using (var fs = new FileStream("C:\\Temp\\" + id + ".blob", FileMode.Create))
            {
                await streamContent.GetStream().CopyToAsync(fs);
                await fs.FlushAsync();
            }
        }

        public async Task CreateFileAsync(CreateFileInput input)
        {
            using (var fs = new FileStream("C:\\Temp\\" + input.Id + ".blob", FileMode.Create))
            {
                await input.Content.GetStream().CopyToAsync(fs);
                await fs.FlushAsync();
            }
        }

        public async Task CreateMultipleFileAsync(CreateMultipleFileInput input)
        {
            using (var fs = new FileStream("C:\\Temp\\" + input.Id + ".blob", FileMode.Append))
            {
                foreach (var content in input.Contents)
                {
                    await content.GetStream().CopyToAsync(fs);
                }
                await fs.FlushAsync();
            }
        }
    }
}

IRemoteStreamContent自动 API 控制器动态 C# HTTP 代理 系统兼容。

生命周期

应用服务的生命周期是 瞬态 的,并且它们自动注册到依赖注入系统。

另请参阅

在本文档中