Entity Framework Core - Basic CRUD Operations
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
| Operation | Standard Method | Bulk/Direct Method (EF Core 7+) |
| Create | AddAsync, SaveChanges | AddRangeAsync (Batch insert) |
| Read | FindAsync, ToListAsync | AsNoTracking (Read-only optimization) |
| Update | Modify property + SaveChanges | ExecuteUpdateAsync |
| Delete | Remove + SaveChanges | ExecuteDeleteAsync |
Best Practices
Always use Async: Use
ToListAsync,SaveChangesAsync, etc., to prevent blocking threads in web applications.Use
AsNoTracking: For "Read Only" pages (like lists or reports), always use.AsNoTracking()to reduce memory usage.Check for Null: Always handle the case where
FindAsyncorFirstOrDefaultAsyncreturns null.

