| name | entity-framework-core |
| description | USE FOR: CRUD data access with LINQ, database migrations, change tracking, complex domain models with relationships, database-first and code-first workflows, and multi-provider support (SQL Server, PostgreSQL, SQLite, Cosmos DB). DO NOT USE FOR: Bulk operations on millions of rows without batching extensions, latency-critical read-only queries where Dapper is faster, or non-relational data that does not fit an entity model.
|
| license | MIT |
| metadata | {"displayName":"Entity Framework Core","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"Entity Framework Core Documentation","url":"https://learn.microsoft.com/en-us/ef/core/"},{"title":"Entity Framework Core GitHub Repository","url":"https://github.com/dotnet/efcore"},{"title":"Microsoft.EntityFrameworkCore NuGet Package","url":"https://www.nuget.org/packages/Microsoft.EntityFrameworkCore"}] |
Entity Framework Core
Overview
Entity Framework Core (EF Core) is Microsoft's official ORM for .NET. It provides LINQ-based querying, change tracking, migrations, and a rich mapping system that translates between C# objects and relational database tables. EF Core supports multiple database providers including SQL Server, PostgreSQL (via Npgsql), SQLite, MySQL (via Pomelo), and Azure Cosmos DB.
EF Core follows the Unit of Work and Repository patterns through DbContext, which tracks entity state changes and generates optimized SQL for inserts, updates, and deletes. The migrations system allows schema evolution to be version-controlled alongside application code.
Install via NuGet: dotnet add package Microsoft.EntityFrameworkCore.SqlServer (or the provider for your database).
DbContext and Entity Configuration
using Microsoft.EntityFrameworkCore;
public sealed class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(p => p.Id);
entity.Property(p => p.Name).HasMaxLength(200).IsRequired();
entity.Property(p => p.Price).HasPrecision(18, 2);
entity.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
entity.HasIndex(p => p.Name);
});
modelBuilder.Entity<Order>(entity =>
{
entity.HasKey(o => o.Id);
entity.Property(o => o.Total).HasPrecision(18, 2);
entity.HasMany(o => o.Items)
.WithOne(i => i.Order)
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = .Empty;
Price { ; ; }
CategoryId { ; ; }
Category Category { ; ; } = !;
}
{
Id { ; ; }
Name { ; ; } = .Empty;
List<Product> Products { ; ; } = ();
}
{
Id { ; ; }
DateTime OrderDate { ; ; }
Total { ; ; }
List<OrderItem> Items { ; ; } = ();
}
{
Id { ; ; }
OrderId { ; ; }
Order Order { ; ; } = !;
ProductId { ; ; }
Quantity { ; ; }
UnitPrice { ; ; }
}
Registration and Connection
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null);
sqlOptions.CommandTimeout(30);
}));
var app = builder.Build();
await app.RunAsync();
Querying with LINQ
using Microsoft.EntityFrameworkCore;
public sealed class ProductService
{
private readonly AppDbContext _db;
public ProductService(AppDbContext db)
{
_db = db;
}
public async Task<Product?> GetByIdAsync(int id, CancellationToken ct)
{
return await _db.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id, ct);
}
public async Task<List<Product>> SearchAsync(
string? name, decimal? minPrice, int page, int pageSize, CancellationToken ct)
{
IQueryable<Product> query = _db.Products.AsNoTracking();
if (!string.IsNullOrWhiteSpace(name))
{
query = query.Where(p => p.Name.Contains(name));
}
if (minPrice.HasValue)
{
query = query.Where(p => p.Price >= minPrice.Value);
}
return await query
.OrderBy(p => p.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(ct);
}
public async Task<List<CategorySummary>> GetCategorySummaryAsync(CancellationToken ct)
{
return await _db.Categories
.AsNoTracking()
.Select(c => new CategorySummary
{
CategoryName = c.Name,
ProductCount = c.Products.Count,
AveragePrice = c.Products.Average(p => (decimal?)p.Price) ?? 0
})
.OrderByDescending(c => c.ProductCount)
.ToListAsync(ct);
}
}
{
CategoryName { ; ; } = .Empty;
ProductCount { ; ; }
AveragePrice { ; ; }
}
Creating, Updating, and Deleting
using Microsoft.EntityFrameworkCore;
public sealed class OrderService
{
private readonly AppDbContext _db;
public OrderService(AppDbContext db)
{
_db = db;
}
public async Task<Order> CreateOrderAsync(
List<(int ProductId, int Quantity)> items, CancellationToken ct)
{
var productIds = items.Select(i => i.ProductId).ToList();
var products = await _db.Products
.Where(p => productIds.Contains(p.Id))
.ToDictionaryAsync(p => p.Id, ct);
var order = new Order
{
OrderDate = DateTime.UtcNow,
Items = items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = products[i.ProductId].Price
}).ToList()
};
order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
return order;
}
public async Task<bool> CancelOrderAsync(int orderId, CancellationToken ct)
{
int deleted = await _db.Orders
.Where(o => o.Id == orderId)
.ExecuteDeleteAsync(ct);
return deleted > 0;
}
Task ()
{
_db.Products
.Where(p => p.CategoryId == categoryId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Price, p => p.Price * ( + percentageIncrease / )),
ct);
}
}
Migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations script --idempotent -o migrate.sql
using Microsoft.EntityFrameworkCore;
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
}
await app.RunAsync();
Interceptors and Auditing
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
public sealed class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context is null) return new ValueTask<InterceptionResult<int>>(result);
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.ModifiedAt = DateTime.UtcNow;
break;
}
}
return new ValueTask<InterceptionResult<int>>(result);
}
}
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime? ModifiedAt { get; set; }
}
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options.UseSqlServer(connectionString)
.AddInterceptors(new AuditSaveChangesInterceptor()));
Best Practices
- Use
AsNoTracking() on all read-only queries to avoid the overhead of change tracking and reduce memory consumption in read-heavy scenarios.
- Keep
DbContext lifetime scoped to the request (the default with AddDbContext) and never register it as a singleton, which causes concurrency issues.
- Use
ExecuteUpdateAsync and ExecuteDeleteAsync for bulk operations instead of loading entities into memory, modifying them, and calling SaveChangesAsync.
- Always use
Include or projection (Select) to load related data explicitly; never rely on lazy loading, which causes N+1 query problems and hides performance issues.
- Generate idempotent SQL scripts with
dotnet ef migrations script --idempotent for production deployments rather than calling Database.MigrateAsync() at startup.
- Configure connection resiliency with
EnableRetryOnFailure for cloud-hosted databases that may experience transient connection failures.
- Use
HasPrecision(18, 2) on all decimal properties in OnModelCreating to avoid silent precision loss when mapping to database column types.
- Add interceptors for cross-cutting concerns like audit timestamps, soft-delete filtering, and query logging rather than scattering that logic across repositories.
- Use
IEntityTypeConfiguration<T> in separate files per entity instead of a single large OnModelCreating method, keeping model configuration modular and testable.
- Add explicit indexes with
HasIndex on columns used in WHERE, JOIN, and ORDER BY clauses to ensure the database engine can serve queries efficiently.