应用服务
应用服务用于实现应用程序的 用例。它们用于 将领域逻辑暴露给展示层。
应用服务从展示层调用(可选),使用 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 类是 BookDto 和 CreateUpdateBookDto:
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 实体,其复合主键为 CityId 和 Name。使用 AbstractKeyCrudAppService 需要您自己实现 DeleteByIdAsync 和 GetEntityByIdAsync 方法:
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 应用服务)
有两种方式对基类应用服务方法进行授权:
- 您可以在服务的构造函数中设置策略属性(xxxPolicyName)。示例:
public class MyPeopleAppService : CrudAppService<Person, PersonDto, Guid>
{
public MyPeopleAppService(IRepository<Person, Guid> repository)
: base(repository)
{
GetPolicyName = "...";
GetListPolicyName = "...";
CreatePolicyName = "...";
UpdatePolicyName = "...";
DeletePolicyName = "...";
}
}
CreatePolicyName 由 CreateAsync 方法检查,依此类推... 您应该指定在应用程序中定义的策略(权限)名称。
- 您可以在服务中覆盖检查方法(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用于将实体映射为从GetAsync、CreateAsync和UpdateAsync方法返回的 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 代理 系统兼容。
生命周期
应用服务的生命周期是 瞬态 的,并且它们自动注册到依赖注入系统。
抠丁客


