mcpField
A
abp-entity-patterns

abp-entity-patterns

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

关于 abp-entity-patterns

面向使用 ABP Framework 和 DDD 构建应用的开发者,提供领域层参考,涵盖实体、仓储、领域服务和数据填充模式,以及审计与多租户指导。

主要功能

  1. 01选择实体与聚合根基类
  2. 02应用审计和软删除模式
  3. 03建模多租户实体
  4. 04定义自定义仓储和领域服务
  5. 05填充应用数据与测试数据

适用场景

  1. 01为实体建模并封装状态与验证
  2. 02跟踪变更并过滤软删除记录
  3. 03为实体添加租户感知行为
  4. 04实现跨实体业务逻辑和自定义查询

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill abp-entity-patterns

安装后在 Claude Code 中输入/abp-entity-patterns确认已加载。

详细介绍

ABP 实体模式

遵循 DDD 原则的 ABP Framework 领域层模式。

架构层

Domain.Shared    → Constants, enums, shared types
Domain           → Entities, repositories, domain services, domain events
Application.Contracts → DTOs, application service interfaces
Application      → Application services, mapper profiles
EntityFrameworkCore → DbContext, repository implementations
HttpApi          → Controllers
HttpApi.Host     → Startup, configuration

核心原则:依赖自上而下流动。应用层依赖领域层,但领域层绝不依赖应用层。

实体基类

选择合适的基类
基类适用场景
Entity<TKey>简单实体,无审计需求
AuditedEntity<TKey>需要跟踪创建和修改
FullAuditedEntity<TKey>需要软删除和完整审计
AggregateRoot<TKey>聚合的根实体
FullAuditedAggregateRoot<TKey>最常用——功能齐全
标准实体模式
public class Patient : FullAuditedAggregateRoot<Guid>
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string Email { get; private set; }
    public DateTime DateOfBirth { get; private set; }
    public bool IsActive { get; private set; }

    // Required for EF Core
    protected Patient() { }

    // Constructor with validation
    public Patient(
        Guid id,
        string firstName,
        string lastName,
        string email,
        DateTime dateOfBirth)
        : base(id)
    {
        SetName(firstName, lastName);
        SetEmail(email);
        DateOfBirth = dateOfBirth;
        IsActive = true;
    }

    // Domain methods with validation
    public void SetName(string firstName, string lastName)
    {
        FirstName = Check.NotNullOrWhiteSpace(firstName, nameof(firstName), maxLength: 100);
        LastName = Check.NotNullOrWhiteSpace(lastName, nameof(lastName), maxLength: 100);
    }

    public void SetEmail(string email)
    {
        Email = Check.NotNullOrWhiteSpace(email, nameof(email), maxLength: 256);
    }

    public void Activate() => IsActive = true;
    public void Deactivate() => IsActive = false;
}
软删除
public class Patient : FullAuditedAggregateRoot<Guid>, ISoftDelete
{
    public bool IsDeleted { get; set; }
    // ABP automatically filters out soft-deleted entities
}
多租户
public class Patient : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
    public Guid? TenantId { get; set; }
    // ABP automatically filters by current tenant
}
审计字段

FullAuditedAggregateRoot<Guid> 提供:

  • CreationTime、CreatorId
  • LastModificationTime、LastModifierId
  • IsDeleted、DeletionTime、DeleterId

仓储模式

通用仓储用法
public class PatientAppService : ApplicationService
{
    private readonly IRepository<Patient, Guid> _patientRepository;

    public PatientAppService(IRepository<Patient, Guid> patientRepository)
    {
        _patientRepository = patientRepository;
    }

    public async Task<PatientDto> GetAsync(Guid id)
    {
        var patient = await _patientRepository.GetAsync(id);
        return ObjectMapper.Map<Patient, PatientDto>(patient);
    }

    public async Task<PagedResultDto<PatientDto>> GetListAsync(PagedAndSortedResultRequestDto input)
    {
        var totalCount = await _patientRepository.GetCountAsync();
        var queryable = await _patientRepository.GetQueryableAsync();

        var patients = await AsyncExecuter.ToListAsync(
            queryable
                .OrderBy(input.Sorting ?? nameof(Patient.FirstName))
                .PageBy(input.SkipCount, input.MaxResultCount));

        return new PagedResultDto<PatientDto>(
            totalCount,
            ObjectMapper.Map<List<Patient>, List<PatientDto>>(patients));
    }
}
自定义仓储

在领域层中定义接口:

public interface IPatientRepository : IRepository<Patient, Guid>
{
    Task<List<Patient>> GetActivePatientsByDoctorAsync(Guid doctorId);
    Task<Patient?> FindByEmailAsync(string email);
}

在 EntityFrameworkCore 层中实现:

public class PatientRepository : EfCoreRepository<ClinicDbContext, Patient, Guid>, IPatientRepository
{
    public PatientRepository(IDbContextProvider<ClinicDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task<List<Patient>> GetActivePatientsByDoctorAsync(Guid doctorId)
    {
        var dbSet = await GetDbSetAsync();
        return await dbSet
            .Where(p => p.PrimaryDoctorId == doctorId && p.IsActive)
            .Include(p => p.Appointments)
            .ToListAsync();
    }

    public async Task<Patient?> FindByEmailAsync(string email)
    {
        var dbSet = await GetDbSetAsync();
        return await dbSet.FirstOrDefaultAsync(p => p.Email == email);
    }
}

领域服务

当业务逻辑涉及多个实体或外部领域概念时,使用领域服务。

public class AppointmentManager : DomainService
{
    private readonly IRepository<Appointment, Guid> _appointmentRepository;
    private readonly IRepository<DoctorSchedule, Guid> _scheduleRepository;

    public AppointmentManager(
        IRepository<Appointment, Guid> appointmentRepository,
        IRepository<DoctorSchedule, Guid> scheduleRepository)
    {
        _appointmentRepository = appointmentRepository;
        _scheduleRepository = scheduleRepository;
    }

    public async Task<Appointment> CreateAsync(
        Guid patientId,
        Guid doctorId,
        DateTime appointmentDate,
        string description)
    {
        // Business rule: Check if doctor is available
        await CheckDoctorAvailabilityAsync(doctorId, appointmentDate);

        // Business rule: Check for conflicts
        await CheckAppointmentConflictsAsync(doctorId, appointmentDate);

        var appointment = new Appointment(
            GuidGenerator.Create(),
            patientId,
            doctorId,
            appointmentDate,
            description);

        return await _appointmentRepository.InsertAsync(appointment);
    }

    private async Task CheckDoctorAvailabilityAsync(Guid doctorId, DateTime appointmentDate)
    {
        var schedule = await _scheduleRepository.FirstOrDefaultAsync(
            s => s.DoctorId == doctorId && s.DayOfWeek == appointmentDate.DayOfWeek);

        if (schedule == null)
            throw new BusinessException("Doctor not available on this day");

        var timeOfDay = appointmentDate.TimeOfDay;
        if (timeOfDay < schedule.StartTime || timeOfDay > schedule.EndTime)
            throw new BusinessException("Doctor not available at this time");
    }

    private async Task CheckAppointmentConflictsAsync(Guid doctorId, DateTime appointmentDate)
    {
        var hasConflict = await _appointmentRepository.AnyAsync(a =>
            a.DoctorId == doctorId &&
            a.AppointmentDate == appointmentDate &&
            a.Status != AppointmentStatus.Cancelled);

        if (hasConflict)
            throw new BusinessException("Doctor already has an appointment at this time");
    }
}

数据播种

IDataSeedContributor 模式
public class ClinicDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    private readonly IRepository<Doctor, Guid> _doctorRepository;
    private readonly IGuidGenerator _guidGenerator;

    public ClinicDataSeedContributor(
        IRepository<Doctor, Guid> doctorRepository,
        IGuidGenerator guidGenerator)
    {
        _doctorRepository = doctorRepository;
        _guidGenerator = guidGenerator;
    }

    public async Task SeedAsync(DataSeedContext context)
    {
        // Idempotent check
        if (await _doctorRepository.GetCountAsync() > 0)
            return;

        var doctors = new List<Doctor>
        {
            new Doctor(_guidGenerator.Create(), "Dr. Smith", "Cardiology", "[email protected]"),
            new Doctor(_guidGenerator.Create(), "Dr. Jones", "Pediatrics", "[email protected]"),
        };

        foreach (var doctor in doctors)
        {
            await _doctorRepository.InsertAsync(doctor);
        }
    }
}
测试数据播种
public class ClinicTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    public static readonly Guid TestPatientId = Guid.Parse("2e701e62-0953-4dd3-910b-dc6cc93ccb0d");
    public static readonly Guid TestDoctorId = Guid.Parse("3a801f73-1064-5ee4-a21c-ed7dd4ddc1e");

    public async Task SeedAsync(DataSeedContext context)
    {
        await _patientRepository.InsertAsync(new Patient(
            TestPatientId, "Test", "Patient", "[email protected]", DateTime.Now.AddYears(-30)));

        await _doctorRepository.InsertAsync(new Doctor(
            TestDoctorId, "Test Doctor", "General", "[email protected]"));
    }
}

最佳实践

  1. 封装状态——使用私有 setter 和领域方法
  2. 在构造函数中验证——确保实体始终有效
  3. 使用值对象——用于复杂属性(Address、Money)
  4. 将领域逻辑放在实体中——简单规则应属于实体
  5. 领域服务——用于跨实体逻辑
  6. 自定义仓储——仅在需要自定义查询时使用
  7. 幂等播种——插入前始终进行检查

相关技能

  • abp-service-patterns——应用层模式
  • abp-infrastructure-patterns——横切关注点
  • efcore-patterns——数据库配置

评论

支持 Markdown