What you will build and learn
This lesson continues from the Web API lesson. Instead of returning hard-coded data, you will learn the data access layer behind a real application: entity classes, a DbContext, migrations, queries, updates, relationships, and database-safe practices.
ExecuteUpdateAsync and ExecuteDeleteAsync are useful in EF Core projects, but the EF Core 10-specific improvement is stronger support for scenarios such as JSON-column updates, named query filters, and newer SQL Server/Azure SQL features.What is Entity Framework Core?
Entity Framework Core is the main object-relational mapper used by many .NET developers. It lets you work with database rows as C# objects while still allowing you to inspect and optimize the SQL that is generated.
In a typical ASP.NET Core application, EF Core sits between your API/service layer and your database. Your controllers or Minimal API endpoints call services, services call the DbContext, and the DbContext translates LINQ queries into database operations.
| EF Core concept | What it means | Example |
|---|---|---|
| Entity | A C# class mapped to a table or collection. | Order, Customer |
| DbContext | The session used to query and save data. | StoreDbContext |
| DbSet | A queryable set of entities. | DbSet<Order> |
| Migration | A versioned database schema change. | AddCustomerEmail |
Create the entity model
Start with small, clear entity classes. Avoid putting too much behavior into the database model at the beginning. A clean first model is easier to migrate, query, test, and explain to Copilot.
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
public List<Order> Orders { get; set; } = [];
}
public class Order
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Status { get; set; } = "Pending";
public decimal Total { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = default!;
public ShippingInfo Shipping { get; set; } = new();
}
The relationship is expressed by CustomerId and the Customer navigation property. EF Core can infer many relationships, but professional projects should configure important behavior explicitly.
Add the DbContext and connection
A DbContext represents a session with the database. It stores the model configuration, exposes DbSet properties, tracks changes, and sends updates to the database when you call SaveChangesAsync.
using Microsoft.EntityFrameworkCore;
public class StoreDbContext(DbContextOptions<StoreDbContext> options)
: DbContext(options)
{
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>()
.HasIndex(c => c.Email)
.IsUnique();
modelBuilder.Entity<Order>()
.Property(o => o.Total)
.HasPrecision(18, 2);
}
}
// Program.cs
builder.Services.AddDbContext<StoreDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("StoreDb")));
Create and review migrations
Migrations let you evolve the database schema as your model changes. The common beginner mistake is to generate migrations and apply them without reading the generated code. For real projects, always review the migration before applying it.
| Task | Command | Purpose |
|---|---|---|
| Add migration | dotnet ef migrations add InitialCreate | Create a versioned schema change. |
| Apply locally | dotnet ef database update | Update your development database. |
| Generate script | dotnet ef migrations script | Review SQL before production deployment. |
| Bundle migration | dotnet ef migrations bundle | Create a deployable migration executable. |
Change the model
Add a property, table, relationship, constraint, or index.
Add a migration
Generate the migration and inspect the Up and Down methods.
Test locally
Apply to a development database and run the app.
Deploy safely
Use reviewed scripts or migration bundles for staging and production.
Query data with LINQ
EF Core translates LINQ queries into SQL. That is powerful, but it also means your C# query shape affects database performance. Start with clear queries, then inspect the SQL for important operations.
var recentOrders = await context.Orders
.Where(o => o.Status == "Pending" && o.CreatedAt > DateTime.UtcNow.AddDays(-30))
.OrderByDescending(o => o.CreatedAt)
.Select(o => new OrderListItemDto(
o.Id,
o.Customer.Name,
o.Total,
o.CreatedAt))
.AsNoTracking()
.ToListAsync();
Select only the fields your screen or API response needs.
AsNoTrackingFor read-only queries, avoid change tracking overhead.
Index columns that appear often in filters, joins, or sorting.
Use logs, database tools, or ToQueryString() for important queries.
Create, update, and delete records
For normal entity updates, load or attach the entity, make changes, and call SaveChangesAsync. EF Core tracks changes and sends the required SQL statements.
var customer = new Customer
{
Name = "Alicia Tan",
Email = "alicia@example.com"
};
context.Customers.Add(customer);
await context.SaveChangesAsync();
var order = await context.Orders.FindAsync(id);
if (order is null) return Results.NotFound();
order.Status = "Paid";
await context.SaveChangesAsync();
return Results.Ok(order);
Configure relationships and delete behavior
Relationships are where many beginner EF Core problems start. Decide clearly whether a relationship is required, optional, one-to-many, one-to-one, or many-to-many. Also decide what happens when a parent record is deleted.
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
DeleteBehavior.Restrict prevents accidental deletion of a customer if orders still reference that customer. This is often safer for business records such as orders, invoices, audit logs, and payments.
Use complex types and JSON columns carefully
Complex types model values that belong to an entity but do not need a separate identity. Examples include address, money, dimensions, settings, or shipment details. EF Core 10 improves document-style modeling by allowing complex types to be mapped to JSON columns in supported relational databases.
public class ShippingInfo
{
public string Recipient { get; set; } = "";
public string Country { get; set; } = "";
public string PostalCode { get; set; } = "";
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.ComplexProperty(o => o.Shipping, s => s.ToJson());
}
var localOrders = await context.Orders
.Where(o => o.Shipping.Country == "Malaysia")
.ToListAsync();
Apply named query filters
Global query filters are useful for soft deletion and multi-tenant systems. EF Core 10 adds named query filters, so you can define multiple filters and selectively disable a specific filter for special administrative queries.
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDelete", o => !o.IsDeleted)
.HasQueryFilter("Tenant", o => o.TenantId == tenantId);
var deletedOrders = await context.Orders
.IgnoreQueryFilters(["SoftDelete"])
.ToListAsync();
This is cleaner than combining every rule into one large filter because you can reason about each concern separately.
Use ExecuteUpdate and ExecuteDelete for set-based operations
When you need to update many rows based on a condition, set-based operations are usually better than loading every row into memory. EF Core 10 also improves bulk update scenarios involving JSON properties.
await context.Orders
.Where(o => o.Status == "Pending" && o.CreatedAt < cutoff)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, "Expired")
.SetProperty(o => o.UpdatedAt, DateTime.UtcNow));
// Example: increment a value inside a JSON-mapped complex type
await context.Blogs.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Details.Views, b => b.Details.Views + 1));
Important EF Core 10 features to know
| Feature | Why it matters | When to use it |
|---|---|---|
| JSON type support | Better JSON storage and querying for SQL Server 2025 and Azure SQL scenarios. | Structured document-like data inside a row. |
| Complex types to JSON | Cleaner value-object modeling without separate identity. | Addresses, settings, shipping details, metadata. |
| Named query filters | Separate soft-delete and tenant filters. | Multi-tenant or administrative applications. |
| LeftJoin and RightJoin support | Simpler LINQ join expressions in .NET 10 queries. | Reports and dashboard-style queries. |
| Vector search support | Supports AI/RAG-style similarity search with SQL Server/Azure SQL capabilities. | Semantic search, recommendations, knowledge-base lookup. |
| Default constraint naming | More predictable database schema objects. | Teams that review generated SQL and database diffs. |
Performance and maintainability checklist
EF Core is productive, but production performance still depends on database knowledge. Use Visual Studio, SQL logs, and your database tools together.
Use ToListAsync, SingleOrDefaultAsync, and SaveChangesAsync in web apps.
Return DTOs instead of loading entire object graphs unnecessarily.
Use projection or Include intentionally, and inspect generated SQL.
Add indexes for common search, join, and sort columns.
Use AsNoTracking for read-only API responses and reports.
Integration-test with the same database engine used in production whenever possible.
Useful Copilot prompts for EF Core
Copilot can help with EF Core, but you should ask for reviewable steps, not silent magic. Good prompts include your database provider, entity names, migration expectations, and performance constraints.
| Task | Prompt you can use |
|---|---|
| Model review | “Review these EF Core entities for relationship mistakes, delete behavior risks, and missing indexes.” |
| Migration safety | “Explain what this migration changes and identify any data-loss risk before I apply it.” |
| Query performance | “Suggest a projection-based version of this query and explain the SQL performance impact.” |
| Testing | “Create integration tests for this repository using the same database provider pattern as the app.” |
Hands-on exercise: Add database access to the Orders API
- Create a new
StoreDbContextwithCustomersandOrders. - Add SQL Server or LocalDB connection configuration.
- Create the first migration named
InitialCreate. - Add endpoints to list, create, update, and delete orders.
- Use DTOs instead of returning EF entities directly.
- Add
AsNoTrackingto read-only queries. - Generate a SQL migration script and review it before applying changes.
What you learned
You learned how EF Core fits into a Visual Studio 2026 application, how to design entities and a DbContext, how to manage migrations, and how to write safer LINQ queries. You also saw EF Core 10 topics such as complex types mapped to JSON, named query filters, JSON bulk updates, LeftJoin/RightJoin support, and vector search awareness.
In the next lesson, you will test this kind of code with xUnit and use Copilot to help understand test failures without blindly accepting generated fixes.
Recommended companion book
Visual Studio 2026 Made Easy gives readers a complete step-by-step path for C#, VB.NET, Python, JavaScript, C++, .NET 10, web apps, databases, debugging, deployment, and AI-assisted development.