From backend-csharp
Use when configuring Entity Framework Core — DbContext setup, entity configurations, relationships, migrations, queries, and repository implementations. Triggers when adding a new entity, configuring mappings, writing LINQ queries, or managing migrations in a C# .NET project.
How this skill is triggered — by the user, by Claude, or both
Slash command
/backend-csharp:csharp-ef-coreThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implement EF Core correctly — typed `DbContext`, `IEntityTypeConfiguration<T>` for all mappings, LINQ queries that translate cleanly to SQL, and safe migration management. Target: EF Core 8 with .NET 8.
Implement EF Core correctly — typed DbContext, IEntityTypeConfiguration<T> for all mappings, LINQ queries that translate cleanly to SQL, and safe migration management. Target: EF Core 8 with .NET 8.
// MyApp.DAL/Context/AppDbContext.cs
public sealed class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply all IEntityTypeConfiguration<T> in this assembly automatically
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
Registration in Program.cs:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
sqlOptions => sqlOptions.EnableRetryOnFailure(3)));
Never use optionsBuilder.UseSqlServer(...) in OnConfiguring — it breaks testability.
Use IEntityTypeConfiguration<T> — never [Column], [Table] DataAnnotations in entity classes (keep entities clean):
// MyApp.DAL/Configurations/OrderConfiguration.cs
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.Id)
.UseIdentityColumn();
builder.Property(o => o.CustomerName)
.IsRequired()
.HasMaxLength(200);
builder.Property(o => o.TotalAmount)
.HasPrecision(18, 2);
builder.Property(o => o.CreatedAt)
.IsRequired()
.HasDefaultValueSql("GETUTCDATE()");
// Owned type (value object)
builder.OwnsOne(o => o.ShippingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(300).IsRequired();
address.Property(a => a.City).HasMaxLength(100).IsRequired();
address.Property(a => a.PostalCode).HasMaxLength(20);
});
// Navigation: one Order has many OrderItems
builder.HasMany(o => o.Items)
.WithOne(i => i.Order)
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
// Index for frequent query pattern
builder.HasIndex(o => o.CustomerName);
builder.HasIndex(o => o.CreatedAt).IsDescending();
}
}
| Relationship | Configuration |
|---|---|
| One-to-many | .HasMany(...).WithOne(...).HasForeignKey(...) |
| One-to-one | .HasOne(...).WithOne(...).HasForeignKey<T>(...) |
| Many-to-many | .HasMany(...).WithMany(...) or implicit with join entity |
| Owned entity | .OwnsOne(...) or .OwnsMany(...) |
// Many-to-many with explicit join entity
builder.HasMany(p => p.Tags)
.WithMany(t => t.Products)
.UsingEntity<ProductTag>(
j => j.HasOne(pt => pt.Tag).WithMany().HasForeignKey(pt => pt.TagId),
j => j.HasOne(pt => pt.Product).WithMany().HasForeignKey(pt => pt.ProductId));
// MyApp.DAL/Repositories/OrderRepository.cs
public sealed class OrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public OrderRepository(AppDbContext context) => _context = context;
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct = default)
=> await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default)
=> await _context.Orders
.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(ct);
public async Task<Order> AddAsync(Order entity, CancellationToken ct = default)
{
await _context.Orders.AddAsync(entity, ct);
await _context.SaveChangesAsync(ct);
return entity;
}
public async Task UpdateAsync(Order entity, CancellationToken ct = default)
{
_context.Orders.Update(entity);
await _context.SaveChangesAsync(ct);
}
public async Task DeleteAsync(int id, CancellationToken ct = default)
{
var entity = await _context.Orders.FindAsync([id], ct)
?? throw new NotFoundException($"Order {id} not found.");
_context.Orders.Remove(entity);
await _context.SaveChangesAsync(ct);
}
}
| Scenario | Guidance |
|---|---|
| Read-only queries | Always use AsNoTracking() |
| Load related data | Include/ThenInclude — avoid lazy loading |
| Large datasets | Use IAsyncEnumerable<T> + AsAsyncEnumerable() |
| Pagination | .Skip((page-1) * size).Take(size) |
| Projection | Use Select(x => new Dto {...}) to avoid over-fetching |
| Count check | Use AnyAsync() not CountAsync() > 0 |
| Single result | FirstOrDefaultAsync > SingleOrDefaultAsync unless uniqueness must be enforced |
// Projection (preferred for read queries)
public async Task<IReadOnlyList<OrderSummaryDto>> GetSummariesAsync(
int page, int pageSize, CancellationToken ct = default)
{
return await _context.Orders
.AsNoTracking()
.Where(o => o.Status == OrderStatus.Active)
.OrderByDescending(o => o.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(o => new OrderSummaryDto(o.Id, o.CustomerName, o.TotalAmount, o.CreatedAt))
.ToListAsync(ct);
}
# Add migration (from solution root)
dotnet ef migrations add <MigrationName> --project src/MyApp.DAL --startup-project src/MyApp.API
# Apply migrations
dotnet ef database update --project src/MyApp.DAL --startup-project src/MyApp.API
# Generate SQL script (for production)
dotnet ef migrations script --idempotent --project src/MyApp.DAL --startup-project src/MyApp.API -o migration.sql
Migration rules:
AddOrderStatusColumn, not ManyChanges)dotnet ef migrations remove only for not-yet-applied migrationsdotnet ef database update in productionpublic abstract class AuditableEntity
{
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public string? UpdatedBy { get; set; }
}
// Auto-populate via SaveChanges override in DbContext
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
{
var utcNow = DateTime.UtcNow;
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
if (entry.State == EntityState.Added)
entry.Entity.CreatedAt = utcNow;
else if (entry.State == EntityState.Modified)
entry.Entity.UpdatedAt = utcNow;
}
return base.SaveChangesAsync(ct);
}
DbContext configured via constructor DbContextOptions<T>, not OnConfiguringApplyConfigurationsFromAssembly used — no inline modelBuilder.Entity<T>() calls in OnModelCreatingIEntityTypeConfiguration<T> (not DataAnnotations)AsNoTracking() on all read-only queries.Skip().Take() — no unbounded queriesInclude used explicitly — lazy loading disabledAddCustomerEmailIndex, not Migration1)string columns have explicit HasMaxLength setdecimal columns have HasPrecision setSaveChangesAsync called once per unit of work, not inside loopsnpx claudepluginhub gagandeepp/software-agent-teams --plugin backend-csharpGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.