一键导入
repository-pattern
Canonical repository and UnitOfWork creation rules for EF Core. Load when creating repositories or UnitOfWork from scratch.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Canonical repository and UnitOfWork creation rules for EF Core. Load when creating repositories or UnitOfWork from scratch.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Structure and rules for .NET projects using Clean Architecture with DDD. Use when the project has complex business domain, multiple use cases, need to test business logic in isolation, or when the system is enterprise, e-commerce, ERP, CRM, or any rich domain. Do not use for simple CRUDs without logic — prefer Vertical Slice in that case. For implementation details, load the required `backend/dotnet/orms/ef-core/*/SKILL.md` leaf skill(s) or your chosen data access skill.
Structure and rules for .NET projects using Hexagonal Architecture (Ports & Adapters). Use when the project has multiple input channels (API, CLI, message queue), need to test the domain in isolation, or when adapters change frequently. Do not use for simple CRUDs without logic — prefer Vertical Slice in that case. For implementation details, load the required `backend/dotnet/orms/ef-core/*/SKILL.md` leaf skill(s) or your chosen data access skill.
Structure and rules for .NET projects using Onion Architecture. Use when the project has a strong domain-centric focus with DDD, aggregate roots, and the Specification pattern. All dependencies point inward toward the domain core. Do not use for simple CRUDs without logic — prefer Vertical Slice in that case. For implementation details, load the required `backend/dotnet/orms/ef-core/*/SKILL.md` leaf skill(s) or your chosen data access skill.
Canonical DbContext setup and registration rules for EF Core. Load when creating or modifying DbContext classes or their DI wiring.
Pure LINQ standards for .NET 10 projects. Covers language operators, query composition, projection patterns, filtering, grouping, and performance best practices over IEnumerable<T> and IQueryable<T>. Load when writing or reviewing LINQ expressions in any layer. DO NOT load expecting EF Core-specific methods (ToListAsync, AsNoTracking, Include, ExecuteUpdateAsync) — those belong to `backend/dotnet/orms/ef-core/query-best-practices/SKILL.md`.
Canonical rules for using AppDbContext DIRECTLY from application code (use cases / handlers) when the project's data-access pattern is Direct DbContext — no repositories, no UnitOfWork. Load when the chosen pattern is Direct DbContext and a use case reads or writes data. This is the counterpart to repository-usage; never load both for the same task.
| name | repository-pattern |
| description | Canonical repository and UnitOfWork creation rules for EF Core. Load when creating repositories or UnitOfWork from scratch. |
| requires | ["backend-dotnet-csharp"] |
| produces | ["repositories","unit-of-work","repository-interfaces"] |
Load this skill only when the project's Data Access pattern is Repository + UnitOfWork.
Repository and UnitOfWork are created together as one unit — never a repository without a UnitOfWork
to persist through it. If the pattern is Direct DbContext, there is no repository layer; use cases
talk to AppDbContext directly (see dbcontext-usage).
.Update() on tracked entitiesCompleteAsync() on UnitOfWorkSaveChangesAsync() directlypublic abstract class GenericRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext AppDbContext;
protected GenericRepository(AppDbContext appDbContext)
{
AppDbContext = appDbContext;
}
public async Task<TEntity?> GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await AppDbContext.Set<TEntity>()
.FirstOrDefaultAsync(BuildIdPredicate(id), ct);
}
public async Task<TEntity?> GetByIdAsNoTrackingAsync(Guid id, CancellationToken ct = default)
{
return await AppDbContext.Set<TEntity>()
.AsNoTracking()
.FirstOrDefaultAsync(BuildIdPredicate(id), ct);
}
public async Task AddAsync(TEntity entity, CancellationToken ct = default)
{
await AppDbContext.Set<TEntity>().AddAsync(entity, ct);
}
public void Delete(TEntity entity)
{
AppDbContext.Set<TEntity>().Remove(entity);
}
private Expression<Func<TEntity, bool>> BuildIdPredicate(Guid id)
{
Microsoft.EntityFrameworkCore.Metadata.IEntityType? entityType = AppDbContext.Model.FindEntityType(typeof(TEntity));
Microsoft.EntityFrameworkCore.Metadata.IProperty? pkProperty = entityType?.FindPrimaryKey()?.Properties.FirstOrDefault();
if (pkProperty is null)
throw new InvalidOperationException($"Entity {typeof(TEntity).Name} has no primary key defined.");
ParameterExpression parameter = Expression.Parameter(typeof(TEntity), "entity");
MemberExpression property = Expression.Property(parameter, pkProperty.Name);
ConstantExpression value = Expression.Constant(id, typeof(Guid));
BinaryExpression equality = Expression.Equal(property, value);
return Expression.Lambda<Func<TEntity, bool>>(equality, parameter);
}
}
public interface IGenericRepository<TEntity> where TEntity : class
{
Task<TEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<TEntity?> GetByIdAsNoTrackingAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
void Delete(TEntity entity);
}
public interface IProductRepository : IGenericRepository<Product>
{
Task<bool> ExistsByNameAsync(string productName, CancellationToken cancellationToken = default);
Task<Product?> GetByNameAsync(string productName, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken cancellationToken = default);
}
public sealed class ProductRepository : GenericRepository<Product>, IProductRepository
{
public ProductRepository(AppDbContext appDbContext) : base(appDbContext)
{
}
public async Task<bool> ExistsByNameAsync(string productName, CancellationToken cancellationToken = default)
{
return await AppDbContext.Products
.AsNoTracking()
.AnyAsync(product => product.Name == productName, cancellationToken);
}
public async Task<Product?> GetByNameAsync(string productName, CancellationToken cancellationToken = default)
{
return await AppDbContext.Products
.FirstOrDefaultAsync(product => product.Name == productName, cancellationToken);
}
public async Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await AppDbContext.Products
.AsNoTracking()
.ToListAsync(cancellationToken);
}
}
public interface IUnitOfWork : IDisposable
{
IProductRepository ProductRepository { get; }
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
Task<SaveResult> CompleteAsync(CancellationToken cancellationToken = default);
}
public sealed class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _appDbContext;
public IProductRepository ProductRepository { get; }
public UnitOfWork(AppDbContext appDbContext, IProductRepository productRepository)
{
_appDbContext = appDbContext;
ProductRepository = productRepository;
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
{
await _appDbContext.Database.BeginTransactionAsync(cancellationToken);
}
public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
{
await _appDbContext.Database.CommitTransactionAsync(cancellationToken);
}
public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
{
await _appDbContext.Database.RollbackTransactionAsync(cancellationToken);
}
public async Task<SaveResult> CompleteAsync(CancellationToken cancellationToken = default)
{
try
{
int rowsAffected = await _appDbContext.SaveChangesAsync(cancellationToken);
return new SaveResult { IsSuccess = true, RowsAffected = rowsAffected, ErrorMessage = string.Empty };
}
catch (DbUpdateException dbUpdateException)
{
return new SaveResult
{
IsSuccess = false,
ErrorMessage = dbUpdateException.InnerException?.Message ?? dbUpdateException.Message
};
}
}
public void Dispose()
{
_appDbContext.Dispose();
}
}
public sealed class SaveResult
{
public bool IsSuccess { get; init; }
public int RowsAffected { get; init; }
public required string ErrorMessage { get; init; }
}
// ❌ NEVER call .Update()
_appDbContext.Products.Update(product);
// ✅ CORRECT — modify tracked entity, then CompleteAsync
product.UpdateName("New Name");
await _unitOfWork.CompleteAsync(cancellationToken);
// ❌ NEVER rely on lazy loading
public class Product
{
public virtual ICollection<Order> Orders { get; set; } = [];
}