mcpField
A
abp-service-patterns

abp-service-patterns

github.com/thapaliyabikendra开发2457 次安装31 周前更新
代码仓库

关于 abp-service-patterns

面向 ABP Framework 开发者的应用层模式指南,涵盖服务设计、映射、工作单元、筛选、日志记录与映射校验。

主要功能

  1. 01应用服务模式
  2. 02Mapperly 对象映射
  3. 03工作单元控制
  4. 04Filter DTO 查询
  5. 05日志记录与输入清理
  6. 06手动映射检查

适用场景

  1. 01组织 ABP 应用服务
  2. 02将实体映射为 DTO
  3. 03构建筛选列表接口
  4. 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 属性错误来源正确来源
IsPutawayCompletedentity.IsInboundCompletedentity.IsPutawayCompleted
UpdatedAtentity.CreatedAtentity.LastModificationTime
CustomerNameentity.ShipperNameentity.CustomerName
TargetDateentity.SourceDateentity.TargetDate

最佳实践

  1. 精简 AppServices - 负责协调,不要实现业务逻辑
  2. 委托给领域层 - 复杂规则使用领域服务
  3. 使用 Mapperly - 使用源代码生成映射以提升性能(避免复制粘贴错误)
  4. WhereIf 模式 - 简洁地实现可选筛选
  5. 结构化日志记录 - 使用一致的格式进行跟踪
  6. 输入清理 - 修剪并规范化输入
  7. 授权 - 始终检查权限
  8. 验证手动映射 - 仔细检查名称相似的属性赋值

相关技能

  • abp-entity-patterns - 领域层模式
  • abp-infrastructure-patterns - 横切关注点
  • fluentvalidation-patterns - 输入验证

评论

支持 Markdown