mcpField
D
dotnet-repository-pattern

dotnet-repository-pattern

github.com/ronnythedev开发7763 次安装15 周前更新
代码仓库

关于 dotnet-repository-pattern

面向采用分层架构的 .NET 开发者,用于生成遵循聚合根边界和 Unit of Work 的仓储。内容涵盖接口、EF Core 实现模板及查询指导。

主要功能

  1. 01Domain 层仓储接口
  2. 02Infrastructure 层 EF Core 实现
  3. 03每个聚合根配置仓储
  4. 04集成 Unit of Work
  5. 05读取查询优化指导

适用场景

  1. 01搭建 .NET 应用的仓储分层
  2. 02为聚合根生成数据访问模式
  3. 03添加子实体与 Specification 查询

安装

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

$npx skills add https://github.com/ronnythedev/dotnet-clean-architecture-skills --skill dotnet-repository-pattern

安装后在 Claude Code 中输入/dotnet-repository-pattern确认已加载。

详细介绍

仓储模式生成器

概述

此技能用于生成提供数据访问抽象的仓储:

  • 领域层中的接口 - 定义数据访问契约
  • 基础设施层中的实现 - 使用 EF Core
  • 每个聚合根一个 - 而非每个实体一个
  • 集成工作单元 - 通过 IUnitOfWork 调用 SaveChanges

快速参考

仓储方法用途返回值
GetByIdAsync按主键检索Entity?
GetByXxxAsync按业务键检索Entity?
GetAllAsync检索全部(谨慎使用)IReadOnlyList<Entity>
Add跟踪新实体void
Update跟踪已修改的实体void
Remove跟踪已删除的实体void
ExistsAsync检查是否存在bool

仓储结构

/Domain/{Aggregate}/
└── I{Entity}Repository.cs          # Interface (Domain layer)

/Infrastructure/Repositories/
└── {Entity}Repository.cs           # Implementation (Infrastructure layer)

模板:仓储接口(领域层)

// src/{name}.domain/{Aggregate}/I{Entity}Repository.cs
namespace {name}.domain.{aggregate};

public interface I{Entity}Repository
{
    // ═══════════════════════════════════════════════════════════════
    // QUERY METHODS
    // ═══════════════════════════════════════════════════════════════
    
    /// <summary>
    /// Gets an entity by its unique identifier
    /// </summary>
    Task<{Entity}?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets an entity by its unique identifier with related entities
    /// </summary>
    Task<{Entity}?> GetByIdWithDetailsAsync(
        Guid id,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets an entity by a unique business key
    /// </summary>
    Task<{Entity}?> GetByNameAsync(
        string name,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets all entities for a parent organization
    /// </summary>
    Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
        Guid organizationId,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets all active entities
    /// </summary>
    Task<IReadOnlyList<{Entity}>> GetAllActiveAsync(
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Checks if an entity exists
    /// </summary>
    Task<bool> ExistsAsync(
        Guid id,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Checks if an entity with the given name exists
    /// </summary>
    Task<bool> ExistsByNameAsync(
        string name,
        CancellationToken cancellationToken = default);

    // ═══════════════════════════════════════════════════════════════
    // COMMAND METHODS (tracking only, no SaveChanges)
    // ═══════════════════════════════════════════════════════════════
    
    /// <summary>
    /// Adds a new entity to the context
    /// </summary>
    void Add({Entity} {entity});

    /// <summary>
    /// Adds multiple entities to the context
    /// </summary>
    void AddRange(IEnumerable<{Entity}> {entities});

    /// <summary>
    /// Updates an existing entity in the context
    /// </summary>
    void Update({Entity} {entity});

    /// <summary>
    /// Removes an entity from the context
    /// </summary>
    void Remove({Entity} {entity});

    /// <summary>
    /// Removes multiple entities from the context
    /// </summary>
    void RemoveRange(IEnumerable<{Entity}> {entities});
}

模板:仓储实现(基础设施层)

// src/{name}.infrastructure/Repositories/{Entity}Repository.cs
using Microsoft.EntityFrameworkCore;
using {name}.domain.{aggregate};

namespace {name}.infrastructure.repositories;

internal sealed class {Entity}Repository : I{Entity}Repository
{
    private readonly ApplicationDbContext _dbContext;

    public {Entity}Repository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    // ═══════════════════════════════════════════════════════════════
    // QUERY METHODS
    // ═══════════════════════════════════════════════════════════════

    public async Task<{Entity}?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
    }

    public async Task<{Entity}?> GetByIdWithDetailsAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .Include(e => e.{ChildEntities})
            .Include(e => e.{OtherRelation})
            .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
    }

    public async Task<{Entity}?> GetByNameAsync(
        string name,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .FirstOrDefaultAsync(
                e => e.Name.ToLower() == name.ToLower(),
                cancellationToken);
    }

    public async Task<IReadOnlyList<{Entity}>> GetByOrganizationIdAsync(
        Guid organizationId,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .Where(e => e.OrganizationId == organizationId)
            .OrderBy(e => e.Name)
            .ToListAsync(cancellationToken);
    }

    public async Task<IReadOnlyList<{Entity}>> GetAllActiveAsync(
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .Where(e => e.IsActive)
            .OrderBy(e => e.Name)
            .ToListAsync(cancellationToken);
    }

    public async Task<bool> ExistsAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .AnyAsync(e => e.Id == id, cancellationToken);
    }

    public async Task<bool> ExistsByNameAsync(
        string name,
        CancellationToken cancellationToken = default)
    {
        return await _dbContext
            .Set<{Entity}>()
            .AnyAsync(
                e => e.Name.ToLower() == name.ToLower(),
                cancellationToken);
    }

    // ═══════════════════════════════════════════════════════════════
    // COMMAND METHODS
    // ═══════════════════════════════════════════════════════════════

    public void Add({Entity} {entity})
    {
        _dbContext.Set<{Entity}>().Add({entity});
    }

    public void AddRange(IEnumerable<{Entity}> {entities})
    {
        _dbContext.Set<{Entity}>().AddRange({entities});
    }

    public void Update({Entity} {entity})
    {
        _dbContext.Set<{Entity}>().Update({entity});
    }

    public void Remove({Entity} {entity})
    {
        _dbContext.Set<{Entity}>().Remove({entity});
    }

    public void RemoveRange(IEnumerable<{Entity}> {entities})
    {
        _dbContext.Set<{Entity}>().RemoveRange({entities});
    }
}

模板:支持访问子实体的仓储

// src/{name}.domain/{Aggregate}/I{Entity}Repository.cs
namespace {name}.domain.{aggregate};

public interface I{Entity}Repository
{
    // Standard methods...

    // ═══════════════════════════════════════════════════════════════
    // CHILD ENTITY QUERIES (accessed through aggregate root)
    // ═══════════════════════════════════════════════════════════════

    /// <summary>
    /// Gets a child entity through its aggregate root
    /// </summary>
    Task<{ChildEntity}?> Get{ChildEntity}ByIdAsync(
        Guid {entity}Id,
        Guid {childEntity}Id,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Gets all child entities for a parent
    /// </summary>
    Task<IReadOnlyList<{ChildEntity}>> Get{ChildEntities}By{Entity}IdAsync(
        Guid {entity}Id,
        CancellationToken cancellationToken = default);
}
// src/{name}.infrastructure/Repositories/{Entity}Repository.cs
internal sealed class {Entity}Repository : I{Entity}Repository
{
    // ... other methods

    public async Task<{ChildEntity}?> Get{ChildEntity}ByIdAsync(
        Guid {entity}Id,
        Guid {childEntity}Id,
        CancellationToken cancellationToken = default)
    {
        var {entity} = await _dbContext
            .Set<{Entity}>()
            .Include(e => e.{ChildEntities})
            .FirstOrDefaultAsync(e => e.Id == {entity}Id, cancellationToken);

        return {entity}?.{ChildEntities}
            .FirstOrDefault(c => c.Id == {childEntity}Id);
    }

    public async Task<IReadOnlyList<{ChildEntity}>> Get{ChildEntities}By{Entity}IdAsync(
        Guid {entity}Id,
        CancellationToken cancellationToken = default)
    {
        var {entity} = await _dbContext
            .Set<{Entity}>()
            .Include(e => e.{ChildEntities})
            .FirstOrDefaultAsync(e => e.Id == {entity}Id, cancellationToken);

        return {entity}?.{ChildEntities}.ToList() 
            ?? new List<{ChildEntity}>();
    }
}

模板:使用规约模式的仓储

// src/{name}.domain/Abstractions/ISpecification.cs
using System.Linq.Expressions;

namespace {name}.domain.abstractions;

public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    List<string> IncludeStrings { get; }
    Expression<Func<T, object>>? OrderBy { get; }
    Expression<Func<T, object>>? OrderByDescending { get; }
    int? Take { get; }
    int? Skip { get; }
    bool IsPagingEnabled { get; }
}
// src/{name}.domain/Abstractions/BaseSpecification.cs
using System.Linq.Expressions;

namespace {name}.domain.abstractions;

public abstract class BaseSpecification<T> : ISpecification<T>
{
    public Expression<Func<T, bool>> Criteria { get; private set; } = _ => true;
    public List<Expression<Func<T, object>>> Includes { get; } = new();
    public List<string> IncludeStrings { get; } = new();
    public Expression<Func<T, object>>? OrderBy { get; private set; }
    public Expression<Func<T, object>>? OrderByDescending { get; private set; }
    public int? Take { get; private set; }
    public int? Skip { get; private set; }
    public bool IsPagingEnabled { get; private set; }

    protected void AddCriteria(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }

    protected void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    protected void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }

    protected void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
    {
        OrderBy = orderByExpression;
    }

    protected void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
    {
        OrderByDescending = orderByDescExpression;
    }

    protected void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
        IsPagingEnabled = true;
    }
}
// src/{name}.domain/{Aggregate}/Specifications/Active{Entities}Specification.cs
using {name}.domain.abstractions;

namespace {name}.domain.{aggregate}.specifications;

public sealed class Active{Entities}Specification : BaseSpecification<{Entity}>
{
    public Active{Entities}Specification()
    {
        AddCriteria(e => e.IsActive);
        ApplyOrderBy(e => e.Name);
    }
}

public sealed class {Entities}ByOrganizationSpecification : BaseSpecification<{Entity}>
{
    public {Entities}ByOrganizationSpecification(Guid organizationId)
    {
        AddCriteria(e => e.OrganizationId == organizationId && e.IsActive);
        AddInclude(e => e.{ChildEntities});
        ApplyOrderBy(e => e.Name);
    }
}
// Repository with specification support
public interface I{Entity}Repository
{
    Task<IReadOnlyList<{Entity}>> GetAsync(
        ISpecification<{Entity}> specification,
        CancellationToken cancellationToken = default);

    Task<{Entity}?> GetFirstOrDefaultAsync(
        ISpecification<{Entity}> specification,
        CancellationToken cancellationToken = default);

    Task<int> CountAsync(
        ISpecification<{Entity}> specification,
        CancellationToken cancellationToken = default);
}

模板:泛型仓储基类(可选)

// src/{name}.infrastructure/Repositories/Repository.cs
using Microsoft.EntityFrameworkCore;
using {name}.domain.abstractions;

namespace {name}.infrastructure.repositories;

internal abstract class Repository<T> where T : Entity
{
    protected readonly ApplicationDbContext DbContext;

    protected Repository(ApplicationDbContext dbContext)
    {
        DbContext = dbContext;
    }

    public async Task<T?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        return await DbContext
            .Set<T>()
            .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
    }

    public void Add(T entity)
    {
        DbContext.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        DbContext.Set<T>().Update(entity);
    }

    public void Remove(T entity)
    {
        DbContext.Set<T>().Remove(entity);
    }
}
// Using the base repository
internal sealed class {Entity}Repository : Repository<{Entity}>, I{Entity}Repository
{
    public {Entity}Repository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    // Add entity-specific methods
    public async Task<{Entity}?> GetByNameAsync(
        string name,
        CancellationToken cancellationToken = default)
    {
        return await DbContext
            .Set<{Entity}>()
            .FirstOrDefaultAsync(
                e => e.Name.ToLower() == name.ToLower(),
                cancellationToken);
    }
}

注册仓储

// src/{name}.infrastructure/DependencyInjection.cs
private static void AddPersistence(IServiceCollection services, IConfiguration configuration)
{
    var connectionString = configuration.GetConnectionString("Database")
        ?? throw new ArgumentNullException(nameof(configuration));

    services.AddDbContext<ApplicationDbContext>(options =>
    {
        options.UseNpgsql(connectionString)
               .UseSnakeCaseNamingConvention();
    });

    // Register Unit of Work
    services.AddScoped<IUnitOfWork>(sp => 
        sp.GetRequiredService<ApplicationDbContext>());

    // Register Repositories
    services.AddScoped<IUserRepository, UserRepository>();
    services.AddScoped<IOrganizationRepository, OrganizationRepository>();
    services.AddScoped<IDepartmentRepository, DepartmentRepository>();
    services.AddScoped<ISurveyRepository, SurveyRepository>();
    // Add more repositories here...

    // Register SQL Connection Factory for Dapper queries
    services.AddSingleton<ISqlConnectionFactory>(_ => 
        new SqlConnectionFactory(connectionString));
}

查询优化模式

对只读查询使用 AsNoTracking
public async Task<IReadOnlyList<{Entity}>> GetAllForDisplayAsync(
    CancellationToken cancellationToken = default)
{
    return await _dbContext
        .Set<{Entity}>()
        .AsNoTracking()  // Performance: no change tracking
        .Where(e => e.IsActive)
        .OrderBy(e => e.Name)
        .ToListAsync(cancellationToken);
}
选择性 Include(避免过量获取)
// ❌ WRONG: Loading everything
public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct)
{
    return await _dbContext
        .Set<{Entity}>()
        .Include(e => e.Children)
        .Include(e => e.Parent)
        .Include(e => e.Logs)  // Potentially thousands of records!
        .FirstOrDefaultAsync(e => e.Id == id, ct);
}

// ✅ CORRECT: Separate methods for different needs
public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct)
{
    return await _dbContext
        .Set<{Entity}>()
        .FirstOrDefaultAsync(e => e.Id == id, ct);
}

public async Task<{Entity}?> GetByIdWithChildrenAsync(Guid id, CancellationToken ct)
{
    return await _dbContext
        .Set<{Entity}>()
        .Include(e => e.Children)
        .FirstOrDefaultAsync(e => e.Id == id, ct);
}
对大型集合使用拆分查询
public async Task<{Entity}?> GetByIdWithAllRelationsAsync(
    Guid id,
    CancellationToken cancellationToken = default)
{
    return await _dbContext
        .Set<{Entity}>()
        .Include(e => e.Children)
        .Include(e => e.OtherRelation)
        .AsSplitQuery()  // Splits into multiple SQL queries
        .FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
}

关键规则

  1. 每个聚合根对应一个仓储 - 而非每个实体一个
  2. 仓储中不调用 SaveChanges - 这是 IUnitOfWork 的职责
  3. 接口位于领域层 - 实现位于基础设施层
  4. 使用 CancellationToken - 所有异步方法都要使用
  5. 未找到时返回 null - 由处理程序决定如何处理
  6. 读取时使用 AsNoTracking - 不修改数据时
  7. 选择性 Include - 不要过量获取
  8. 避免不加筛选条件地调用 GetAll - 数据规模增大时可能有风险
  9. 通过聚合访问子实体 - 不要公开子实体仓储
  10. 实现类使用 internal - 隐藏实现细节

应避免的反模式

// ❌ WRONG: SaveChanges in repository
public void Add({Entity} {entity})
{
    _dbContext.Set<{Entity}>().Add({entity});
    _dbContext.SaveChanges();  // Don't do this!
}

// ✅ CORRECT: Only track, save via UnitOfWork
public void Add({Entity} {entity})
{
    _dbContext.Set<{Entity}>().Add({entity});
}
// In handler:
await _unitOfWork.SaveChangesAsync(ct);

// ❌ WRONG: Repository for child entities
public interface IOrderItemRepository { ... }

// ✅ CORRECT: Access through aggregate root
public interface IOrderRepository
{
    Task<OrderItem?> GetOrderItemAsync(Guid orderId, Guid itemId, ...);
}

// ❌ WRONG: Exposing IQueryable
public IQueryable<{Entity}> GetAll() => _dbContext.Set<{Entity}>();

// ✅ CORRECT: Return materialized lists
public async Task<IReadOnlyList<{Entity}>> GetAllAsync(CancellationToken ct)
{
    return await _dbContext.Set<{Entity}>().ToListAsync(ct);
}

// ❌ WRONG: Business logic in repository
public async Task<{Entity}?> GetActiveByIdAsync(Guid id, CancellationToken ct)
{
    var entity = await GetByIdAsync(id, ct);
    if (entity?.IsActive == false)
        throw new BusinessException("Entity is inactive");  // Wrong!
    return entity;
}

// ✅ CORRECT: Let handler handle business logic
public async Task<{Entity}?> GetByIdAsync(Guid id, CancellationToken ct)
{
    return await _dbContext.Set<{Entity}>()
        .FirstOrDefaultAsync(e => e.Id == id, ct);
}

相关技能

  • dotnet-domain-entity-generator - 为仓储生成实体
  • dotnet-ef-core-configuration - 配置实体映射
  • dotnet-cqrs-command-generator - 在处理程序中使用仓储
  • dotnet-clean-architecture - 整体项目结构

评论

支持 Markdown