Lesson 6 of 40 Data Access Intermediate 55 min

Entity Framework Core 10

Connect your ASP.NET Core apps to real databases using Entity Framework Core 10. Learn DbContext, entities, migrations, LINQ queries, relationships, JSON columns, named query filters, safe schema updates, and performance habits inside Visual Studio 2026.

DbContextCentral data access session
LINQQuery data with C# expressions
MigrationsVersion database schema safely
Visual Studio 2026 · StoreDbContext
DbSetOrders
DbSetCustomers
JSONShippingInfo
LINQQueries
SQLMigrations
var orders = await context.Orders
  .Where(o => o.Status == "Pending")
  .Include(o => o.Customer)
  .AsNoTracking()
  .ToListAsync();

// EF Core translates LINQ into SQL
// Visual Studio helps debug queries, migrations, and data models
Lesson overview

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.

Model dataCreate entities, value objects, and relationships.
Query safelyUse LINQ, projection, tracking choices, and filters.
Deploy carefullyReview migrations before schema changes reach production.
Important correction: Bulk update APIs such as 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.
Part 1

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 conceptWhat it meansExample
EntityA C# class mapped to a table or collection.Order, Customer
DbContextThe session used to query and save data.StoreDbContext
DbSetA queryable set of entities.DbSet<Order>
MigrationA versioned database schema change.AddCustomerEmail
Part 2

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.

Part 3

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")));
Do not hard-code passwords. Store connection strings in app settings, user secrets, environment variables, Azure Key Vault, or your production configuration system.
Part 4

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.

TaskCommandPurpose
Add migrationdotnet ef migrations add InitialCreateCreate a versioned schema change.
Apply locallydotnet ef database updateUpdate your development database.
Generate scriptdotnet ef migrations scriptReview SQL before production deployment.
Bundle migrationdotnet ef migrations bundleCreate a deployable migration executable.
1

Change the model

Add a property, table, relationship, constraint, or index.

2

Add a migration

Generate the migration and inspect the Up and Down methods.

3

Test locally

Apply to a development database and run the app.

4

Deploy safely

Use reviewed scripts or migration bundles for staging and production.

Part 5

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();
Use projection

Select only the fields your screen or API response needs.

Use AsNoTracking

For read-only queries, avoid change tracking overhead.

Use indexes

Index columns that appear often in filters, joins, or sorting.

Inspect SQL

Use logs, database tools, or ToQueryString() for important queries.

Part 6

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);
For APIs: Avoid returning EF entities directly from public endpoints in larger apps. Use DTOs so you control the response shape and avoid accidental exposure of navigation properties.
Part 7

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.

Part 8

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();
Use JSON for the right reason. JSON columns are useful for structured data that naturally belongs to one row. Do not use them to hide relational data that really needs tables, indexes, joins, and constraints.
Part 9

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.

Part 10

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));
Remember: Set-based updates bypass normal entity change tracking for individual objects. Use them for clear batch operations and document why they are safe.
Part 11

Important EF Core 10 features to know

FeatureWhy it mattersWhen to use it
JSON type supportBetter JSON storage and querying for SQL Server 2025 and Azure SQL scenarios.Structured document-like data inside a row.
Complex types to JSONCleaner value-object modeling without separate identity.Addresses, settings, shipping details, metadata.
Named query filtersSeparate soft-delete and tenant filters.Multi-tenant or administrative applications.
LeftJoin and RightJoin supportSimpler LINQ join expressions in .NET 10 queries.Reports and dashboard-style queries.
Vector search supportSupports AI/RAG-style similarity search with SQL Server/Azure SQL capabilities.Semantic search, recommendations, knowledge-base lookup.
Default constraint namingMore predictable database schema objects.Teams that review generated SQL and database diffs.
Part 12

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.

Prefer async methods

Use ToListAsync, SingleOrDefaultAsync, and SaveChangesAsync in web apps.

Use projection

Return DTOs instead of loading entire object graphs unnecessarily.

Avoid N+1 queries

Use projection or Include intentionally, and inspect generated SQL.

Use indexes

Add indexes for common search, join, and sort columns.

Limit tracking

Use AsNoTracking for read-only API responses and reports.

Test on real provider

Integration-test with the same database engine used in production whenever possible.

Part 13

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.

TaskPrompt 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

  1. Create a new StoreDbContext with Customers and Orders.
  2. Add SQL Server or LocalDB connection configuration.
  3. Create the first migration named InitialCreate.
  4. Add endpoints to list, create, update, and delete orders.
  5. Use DTOs instead of returning EF entities directly.
  6. Add AsNoTracking to read-only queries.
  7. Generate a SQL migration script and review it before applying changes.
Summary

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.

Visual Studio 2026 Made Easy book cover

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.