关于 abp-service-patterns
面向 ABP Framework 开发者的应用层模式指南,涵盖服务设计、映射、工作单元、筛选、日志记录与映射校验。
主要功能
- 01应用服务模式
- 02Mapperly 对象映射
- 03工作单元控制
- 04Filter DTO 查询
- 05日志记录与输入清理
- 06手动映射检查
适用场景
- 01组织 ABP 应用服务
- 02将实体映射为 DTO
- 03构建筛选列表接口
- 04检查手动属性映射
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ thapaliyabikendra/ ai-artifacts --skill abp-service-patterns安装后在 Claude Code 中输入/abp-service-patterns确认已加载。
详细介绍
ABP 服务模式
ABP Framework 的应用层模式。
应用服务模式
public class PatientAppService : ApplicationService, IPatientAppService
{
private readonly IRepository<Patient, Guid> _patientRepository;
private readonly PatientManager _patientManager; // Domain service
private readonly ClinicApplicationMappers _mapper;
public PatientAppService(
IRepository<Patient, Guid> patientRepository,
PatientManager patientManager,
ClinicApplicationMappers mapper)
{
_patientRepository = patientRepository;
_patientManager = patientManager;
_mapper = mapper;
}
[Authorize(ClinicPermissions.Patients.Default)]
public async Task<PatientDto> GetAsync(Guid id)
{
var patient = await _patientRepository.GetAsync(id);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Create)]
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
var patient = await _patientManager.CreateAsync(
input.FirstName, input.LastName, input.Email, input.DateOfBirth);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Edit)]
public async Task<PatientDto> UpdateAsync(Guid id, UpdatePatientDto input)
{
var patient = await _patientRepository.GetAsync(id);
_mapper.UpdatePatientFromDto(input, patient);
await _patientRepository.UpdateAsync(patient);
return _mapper.PatientToDto(patient);
}
[Authorize(ClinicPermissions.Patients.Delete)]
public async Task DeleteAsync(Guid id)
{
await _patientRepository.DeleteAsync(id);
}
}
使用 Mapperly 进行对象映射
ABP 10.x 使用 Mapperly(源代码生成器)替代 AutoMapper。
// Application/ClinicApplicationMappers.cs
[Mapper]
public partial class ClinicApplicationMappers
{
// Entity to DTO
public partial PatientDto PatientToDto(Patient patient);
public partial List<PatientDto> PatientsToDtos(List<Patient> patients);
// DTO to Entity (creation)
public partial Patient CreateDtoToPatient(CreatePatientDto dto);
// DTO to Entity (update) - ignores Id
[MapperIgnoreTarget(nameof(Patient.Id))]
public partial void UpdatePatientFromDto(UpdatePatientDto dto, Patient patient);
// Complex mapping with navigation properties
[MapProperty(nameof(Appointment.Patient.FirstName), nameof(AppointmentDto.PatientName))]
[MapProperty(nameof(Appointment.Doctor.FullName), nameof(AppointmentDto.DoctorName))]
public partial AppointmentDto AppointmentToDto(Appointment appointment);
}
在模块中注册:
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<ClinicApplicationMappers>();
}
工作单元
ABP 会自动管理应用服务方法的 UoW。
public class AppointmentAppService : ApplicationService
{
// This method is automatically wrapped in a UoW
// All changes are committed together or rolled back on exception
public async Task<AppointmentDto> CreateAsync(CreateAppointmentDto input)
{
var patient = await _patientRepository.GetAsync(input.PatientId);
patient.LastAppointmentDate = input.AppointmentDate;
var appointment = new Appointment(
GuidGenerator.Create(),
input.PatientId,
input.DoctorId,
input.AppointmentDate);
await _appointmentRepository.InsertAsync(appointment);
// Both changes committed together automatically
return _mapper.AppointmentToDto(appointment);
}
}
手动控制 UoW:
[UnitOfWork(isTransactional: false)] // Disable for read-only
public async Task GenerateLargeReportAsync() { }
public async Task ProcessBatchAsync(List<Guid> ids)
{
foreach (var id in ids)
{
using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
{
await ProcessItemAsync(id);
await uow.CompleteAsync();
}
}
}
筛选 DTO 模式
将查询筛选条件与分页分开,让 API 简洁且易于自我说明。
筛选 DTO:
public class PatientFilter
{
public Guid? DoctorId { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public bool? IsActive { get; set; }
public DateTime? CreatedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }
}
使用 WhereIf 的 AppService:
public async Task<PagedResultDto<PatientDto>> GetListAsync(
PagedAndSortedResultRequestDto input,
PatientFilter filter)
{
// Trim string inputs
filter.Name = filter.Name?.Trim();
filter.Email = filter.Email?.Trim();
// Default sorting
if (input.Sorting.IsNullOrWhiteSpace())
input.Sorting = nameof(PatientDto.FirstName);
var queryable = await _patientRepository.GetQueryableAsync();
var query = queryable
.WhereIf(filter.DoctorId.HasValue, x => x.DoctorId == filter.DoctorId)
.WhereIf(!filter.Name.IsNullOrWhiteSpace(),
x => x.FirstName.Contains(filter.Name) || x.LastName.Contains(filter.Name))
.WhereIf(!filter.Email.IsNullOrWhiteSpace(),
x => x.Email.ToLower().Contains(filter.Email.ToLower()))
.WhereIf(filter.IsActive.HasValue, x => x.IsActive == filter.IsActive)
.WhereIf(filter.CreatedAfter.HasValue, x => x.CreationTime >= filter.CreatedAfter)
.WhereIf(filter.CreatedBefore.HasValue, x => x.CreationTime <= filter.CreatedBefore);
var totalCount = await AsyncExecuter.CountAsync(query);
var patients = await AsyncExecuter.ToListAsync(
query.OrderBy(input.Sorting).PageBy(input.SkipCount, input.MaxResultCount));
return new PagedResultDto<PatientDto>(totalCount, _mapper.PatientsToDtos(patients));
}
ResponseModel 包装器
public class ResponseModel<T>
{
public bool IsSuccess { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public static ResponseModel<T> Success(T data, string message = null)
=> new() { IsSuccess = true, Data = data, Message = message };
public static ResponseModel<T> Failure(string message)
=> new() { IsSuccess = false, Message = message };
}
// Usage
public async Task<ResponseModel<PatientDto>> GetAsync(Guid id)
{
var patient = await _patientRepository.FirstOrDefaultAsync(x => x.Id == id);
if (patient == null)
return ResponseModel<PatientDto>.Failure("Patient not found");
return ResponseModel<PatientDto>.Success(_mapper.PatientToDto(patient));
}
CommonDependencies 模式
通过将横切关注点依赖项分组,减少构造函数中的依赖项。
public class CommonDependencies<T>
{
public IDistributedEventBus DistributedEventBus { get; set; }
public IDataFilter DataFilter { get; set; }
public ILogger<T> Logger { get; set; }
public IGuidGenerator GuidGenerator { get; set; }
}
// Register
context.Services.AddTransient(typeof(CommonDependencies<>));
// Usage
public class PatientAppService : ApplicationService
{
private readonly IRepository<Patient, Guid> _patientRepository;
private readonly CommonDependencies<PatientAppService> _common;
public PatientAppService(
IRepository<Patient, Guid> patientRepository,
CommonDependencies<PatientAppService> common)
{
_patientRepository = patientRepository;
_common = common;
}
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
_common.Logger.LogInformation("Creating patient: {Name}", input.FirstName);
var patient = new Patient(_common.GuidGenerator.Create(), /*...*/);
await _patientRepository.InsertAsync(patient);
await _common.DistributedEventBus.PublishAsync(new PatientCreatedEto { Id = patient.Id });
return _mapper.PatientToDto(patient);
}
}
结构化日志记录
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
_logger.LogInformation(
"[{Service}] {Method} - Started - Input: {@Input}",
nameof(PatientAppService), nameof(CreateAsync), input);
try
{
var patient = await _patientManager.CreateAsync(/*...*/);
_logger.LogInformation(
"[{Service}] {Method} - Completed - PatientId: {PatientId}",
nameof(PatientAppService), nameof(CreateAsync), patient.Id);
return _mapper.PatientToDto(patient);
}
catch (Exception ex)
{
_logger.LogError(ex,
"[{Service}] {Method} - Failed - Error: {Message}",
nameof(PatientAppService), nameof(CreateAsync), ex.Message);
throw;
}
}
输入清理
public static class InputSanitization
{
public static string TrimAndLower(this string value) => value?.Trim()?.ToLowerInvariant();
public static string TrimAndUpper(this string value) => value?.Trim()?.ToUpperInvariant();
}
// Usage
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
input.Email = input.Email.TrimAndLower();
input.FirstName = input.FirstName?.Trim();
// ...
}
映射验证模式
常见错误:复制粘贴属性映射
手动映射(尤其是在 select new 子句中)容易出现复制粘贴错误:
// ❌ BUG: Wrong property copied - IsPutawayCompleted mapped from wrong source!
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsInboundQCChecklistCompleted, // BUG! Should be lc.IsPutawayCompleted
IsHold = lc.IsHold
}
// ✅ CORRECT: Use Mapperly to prevent copy-paste errors
[Mapper]
public partial class LicensePlateMapper
{
public partial LicensePlateDto ToDto(LicensePlate entity);
}
// Or if manual mapping is required, double-check similar-named properties
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsPutawayCompleted, // ✅ Correct property
IsHold = lc.IsHold
}
手动映射检查清单
在无法避免手动映射时(例如,复杂投影),请确认:
- 每个 DTO 属性都映射到正确的实体属性
- 再次检查名称相似的属性(例如,
IsXxxCompleted与IsYyyCompleted) - 对可选导航属性进行空值检查
- 不要未经修改就复制粘贴相邻行
高风险属性模式
对于以下看起来相似的模式,请格外小心:
| DTO 属性 | 错误来源 | 正确来源 |
|---|---|---|
IsPutawayCompleted | entity.IsInboundCompleted | entity.IsPutawayCompleted |
UpdatedAt | entity.CreatedAt | entity.LastModificationTime |
CustomerName | entity.ShipperName | entity.CustomerName |
TargetDate | entity.SourceDate | entity.TargetDate |
最佳实践
- 精简 AppServices - 负责协调,不要实现业务逻辑
- 委托给领域层 - 复杂规则使用领域服务
- 使用 Mapperly - 使用源代码生成映射以提升性能(避免复制粘贴错误)
- WhereIf 模式 - 简洁地实现可选筛选
- 结构化日志记录 - 使用一致的格式进行跟踪
- 输入清理 - 修剪并规范化输入
- 授权 - 始终检查权限
- 验证手动映射 - 仔细检查名称相似的属性赋值
相关技能
abp-entity-patterns- 领域层模式abp-infrastructure-patterns- 横切关注点fluentvalidation-patterns- 输入验证
评论
支持 Markdown