Skip to main content

Command Palette

Search for a command to run...

Entity Framework Core - Basic CRUD Operations

Updated
5 min readView as Markdown

This guide assumes you have a basic Entity (Model) and a DbContext set up. Otherwise follow this document for setup.

Prerequisites: The Setup

For all examples below, we will use the following Product entity and AppDbContext.

// The Entity
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool IsActive { get; set; }
}

// The Context
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    // ... configuration ...
}

1. Create (Insert)

Adding data typically involves adding an object to the DbSet and calling SaveChanges.

A. Single Insert

Use Add or AddAsync to start tracking a new entity.

public async Task CreateProductAsync(AppDbContext context)
{
    var newProduct = new Product
    {
        Name = "Laptop",
        Price = 999.99m,
        IsActive = true
    };

    // Adds the entity to the ChangeTracker with 'Added' state
    await context.Products.AddAsync(newProduct);

    // Persists changes to the database
    await context.SaveChangesAsync();
}

B. Multiple Inserts (Bulk)

Use AddRange or AddRangeAsync to insert a list of entities efficiently in a single batch.

public async Task CreateMultipleProductsAsync(AppDbContext context)
{
    var products = new List<Product>
    {
        new Product { Name = "Mouse", Price = 25.00m },
        new Product { Name = "Keyboard", Price = 45.00m }
    };

    await context.Products.AddRangeAsync(products);
    await context.SaveChangesAsync();
}

2. Read (Retrieve)

Reading data can be done via LINQ queries.

A. Get by Primary Key

The FindAsync method is optimized; it checks the local cache (memory) first before querying the database.

public async Task<Product?> GetProductByIdAsync(AppDbContext context, int id)
{
    // Returns null if not found
    return await context.Products.FindAsync(id);
}

B. Get All / List

Use ToListAsync to execute the query and materialize the results.

public async Task<List<Product>> GetAllProductsAsync(AppDbContext context)
{
    return await context.Products.ToListAsync();
}

C. Filtering (Where)

Use standard LINQ Where clauses to filter data.

public async Task<List<Product>> GetActiveProductsAsync(AppDbContext context)
{
    return await context.Products
        .Where(p => p.IsActive && p.Price > 100)
        .ToListAsync();
}

D. Ordering and Fetching Single Items

Use OrderBy, First, Single, etc.

  • FirstOrDefault: Returns the first match or null (safest choice).

  • SingleOrDefault: Returns the match or null, but throws an exception if more than one match exists.

public async Task<Product?> GetCheapestProductAsync(AppDbContext context)
{
    return await context.Products
        .OrderBy(p => p.Price) // Sort ascending
        .FirstOrDefaultAsync();
}

E. Read-Only (Performance Optimization)

If you do not intend to update the data, use AsNoTracking(). This bypasses the Change Tracker, significantly improving performance.

public async Task<List<Product>> GetReadOnlyProductsAsync(AppDbContext context)
{
    return await context.Products
        .AsNoTracking() // Faster, strictly for reading
        .ToListAsync();
}

3. Update

In EF Core, updates usually follow a "retrieve, modify, save" pattern.

A. Standard Update (Tracking)

When you fetch data normally (without AsNoTracking), EF Core "tracks" the object. Any changes to properties are detected automatically when SaveChanges is called.

public async Task UpdateProductPriceAsync(AppDbContext context, int id, decimal newPrice)
{
    // 1. Retrieve
    var product = await context.Products.FindAsync(id);

    if (product != null)
    {
        // 2. Modify
        product.Price = newPrice;

        // 3. Save (EF Core detects the property change automatically)
        await context.SaveChangesAsync();
    }
}

B. Update Entire Object

If you have an object and want to force an update for all columns (common in disconnected scenarios like Web APIs), use Update.

public async Task UpdateEntireProductAsync(AppDbContext context, Product product)
{
    // Marks the entity as 'Modified', all properties will be updated in DB
    context.Products.Update(product);
    await context.SaveChangesAsync();
}

C. Bulk Update (EF Core 7+)

Newer versions of EF Core allow updating without loading data into memory first using ExecuteUpdateAsync. This is highly efficient.

public async Task InflatePricesAsync(AppDbContext context)
{
    // Increases price by 10% for all active products directly in SQL
    await context.Products
        .Where(p => p.IsActive)
        .ExecuteUpdateAsync(s => s
            .SetProperty(p => p.Price, p => p.Price * 1.1m));
}

4. Delete

A. Standard Delete (Tracking)

Retrieve the entity and then remove it.

public async Task DeleteProductAsync(AppDbContext context, int id)
{
    var product = await context.Products.FindAsync(id);
    if (product != null)
    {
        context.Products.Remove(product);
        await context.SaveChangesAsync();
    }
}

B. Delete without Retrieve (Stubbing)

If you know the ID, you can create a "stub" to delete it without fetching it from the DB first (saves one DB roundtrip).

public async Task DeleteProductEfficientlyAsync(AppDbContext context, int id)
{
    var productStub = new Product { Id = id };
    context.Products.Remove(productStub); // EF only needs the ID to delete
    await context.SaveChangesAsync();
}

C. Bulk Delete (EF Core 7+)

Use ExecuteDeleteAsync to delete matching rows directly in the database without loading them.

public async Task DeleteInactiveProductsAsync(AppDbContext context)
{
    // Deletes all rows matching the criteria directly in SQL
    await context.Products
        .Where(p => !p.IsActive)
        .ExecuteDeleteAsync();
}

Summary of Methods

OperationStandard MethodBulk/Direct Method (EF Core 7+)
CreateAddAsync, SaveChangesAddRangeAsync (Batch insert)
ReadFindAsync, ToListAsyncAsNoTracking (Read-only optimization)
UpdateModify property + SaveChangesExecuteUpdateAsync
DeleteRemove + SaveChangesExecuteDeleteAsync

Best Practices

  1. Always use Async: Use ToListAsync, SaveChangesAsync, etc., to prevent blocking threads in web applications.

  2. Use AsNoTracking: For "Read Only" pages (like lists or reports), always use .AsNoTracking() to reduce memory usage.

  3. Check for Null: Always handle the case where FindAsync or FirstOrDefaultAsync returns null.

More from this blog

E

EF Core

31 posts