abp-service-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseABP Service Patterns
ABP服务模式
Application layer patterns for ABP Framework.
适用于ABP Framework的应用层模式。
Application Service Pattern
应用服务模式
csharp
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);
}
}csharp
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);
}
}Object Mapping with Mapperly
使用Mapperly进行对象映射
ABP 10.x uses Mapperly (source generator) instead of AutoMapper.
csharp
// 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);
}Register in Module:
csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<ClinicApplicationMappers>();
}ABP 10.x版本使用Mapperly(源代码生成器)替代AutoMapper。
csharp
// 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);
}在模块中注册:
csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<ClinicApplicationMappers>();
}Unit of Work
工作单元(Unit of Work)
ABP automatically manages UoW for application service methods.
csharp
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);
}
}Manual UoW Control:
csharp
[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();
}
}
}ABP会自动为应用服务方法管理工作单元(UoW)。
csharp
public class AppointmentAppService : ApplicationService
{
// 此方法会自动被工作单元包裹
// 所有变更会一起提交,发生异常时则回滚
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);
// 两处变更会自动一起提交
return _mapper.AppointmentToDto(appointment);
}
}手动控制工作单元:
csharp
[UnitOfWork(isTransactional: false)] // 为只读操作禁用事务
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();
}
}
}Filter DTO Pattern
筛选DTO模式
Separate query filters from pagination for clean, self-documenting APIs.
Filter DTO:
csharp
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; }
}AppService with WhereIf:
csharp
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));
}将查询筛选与分页分离,打造清晰、自文档化的API。
筛选DTO:
csharp
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的应用服务:
csharp
public async Task<PagedResultDto<PatientDto>> GetListAsync(
PagedAndSortedResultRequestDto input,
PatientFilter filter)
{
// 修剪字符串输入
filter.Name = filter.Name?.Trim();
filter.Email = filter.Email?.Trim();
// 默认排序
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 Wrapper
响应模型封装
csharp
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));
}csharp
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 };
}
// 使用示例
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 Pattern
通用依赖模式
Reduce constructor bloat by grouping cross-cutting dependencies.
csharp
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);
}
}通过分组横切关注点来减少构造函数的冗余。
csharp
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; }
}
// 注册
context.Services.AddTransient(typeof(CommonDependencies<>));
// 使用示例
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);
}
}Structured Logging
结构化日志
csharp
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;
}
}csharp
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;
}
}Input Sanitization
输入清理
csharp
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();
// ...
}csharp
public static class InputSanitization
{
public static string TrimAndLower(this string value) => value?.Trim()?.ToLowerInvariant();
public static string TrimAndUpper(this string value) => value?.Trim()?.ToUpperInvariant();
}
// 使用示例
public async Task<PatientDto> CreateAsync(CreatePatientDto input)
{
input.Email = input.Email.TrimAndLower();
input.FirstName = input.FirstName?.Trim();
// ...
}Mapping Validation Patterns
映射验证模式
Common Bug: Copy-Paste Property Mapping
常见问题:复制粘贴属性映射
Manual mappings (especially in clauses) are prone to copy-paste errors:
select newcsharp
// ❌ 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
}手动映射(尤其是在语句中)容易出现复制粘贴错误:
select newcsharp
// ❌ 错误:复制了错误的属性 - IsPutawayCompleted映射自错误的源!
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsInboundQCChecklistCompleted, // 错误!应该是lc.IsPutawayCompleted
IsHold = lc.IsHold
}
// ✅ 正确:使用Mapperly避免复制粘贴错误
[Mapper]
public partial class LicensePlateMapper
{
public partial LicensePlateDto ToDto(LicensePlate entity);
}
// 如果必须手动映射,请仔细检查名称相似的属性
select new LicensePlateDto()
{
IsInboundQCChecklistCompleted = lc.IsInboundQCChecklistCompleted,
IsPutawayCompleted = lc.IsPutawayCompleted, // ✅ 正确的属性
IsHold = lc.IsHold
}Manual Mapping Checklist
手动映射检查清单
When manual mapping is unavoidable (e.g., complex projections), verify:
- Each DTO property maps to the correct entity property
- Similar-named properties double-checked (e.g., vs
IsXxxCompleted)IsYyyCompleted - Null checks on optional navigation properties
- No copy-paste from adjacent lines without modification
当不可避免需要手动映射时(例如复杂投影),请验证:
- 每个DTO属性都映射到正确的实体属性
- 仔细检查名称相似的属性(例如与
IsXxxCompleted)IsYyyCompleted - 对可选导航属性进行空值检查
- 相邻行复制粘贴后未修改的内容要确认无误
High-Risk Property Patterns
高风险属性模式
Be extra careful with these patterns that look similar:
| DTO Property | Wrong Source | Correct Source |
|---|---|---|
| | |
| | |
| | |
| | |
请特别注意这些名称相似的属性模式:
| DTO属性 | 错误的源属性 | 正确的源属性 |
|---|---|---|
| | |
| | |
| | |
| | |
Best Practices
最佳实践
- Thin AppServices - Orchestrate, don't implement business logic
- Delegate to Domain - Use domain services for complex rules
- Use Mapperly - Source-generated mapping for performance (prevents copy-paste bugs)
- WhereIf pattern - Clean optional filtering
- Structured logging - Consistent format for tracing
- Input sanitization - Trim and normalize inputs
- Authorization - Always check permissions
- Verify manual mappings - Double-check similar-named property assignments
- 精简AppServices - 仅做编排,不实现业务逻辑
- 委托给领域层 - 复杂规则使用领域服务处理
- 使用Mapperly - 源代码生成的映射性能更高,还能避免复制粘贴错误
- WhereIf模式 - 实现简洁的可选筛选
- 结构化日志 - 采用一致的格式便于追踪
- 输入清理 - 修剪并标准化输入内容
- 授权验证 - 始终检查权限
- 验证手动映射 - 仔细检查名称相似的属性赋值
Related Skills
相关技能
- - Domain layer patterns
abp-entity-patterns - - Cross-cutting concerns
abp-infrastructure-patterns - - Input validation
fluentvalidation-patterns
- - 领域层模式
abp-entity-patterns - - 横切关注点
abp-infrastructure-patterns - - 输入验证
fluentvalidation-patterns