Introduction
Data access is at the heart of most applications. Entity Framework Core (EF Core) is Microsoft’s modern, lightweight, and extensible ORM (Object-Relational Mapper) for .NET. It simplifies database interactions by allowing developers to work with data using C# objects instead of raw SQL queries.
This article explores best practices for EF Core data access and demonstrates them with a real-time example: a simple Customer Management System.
Why EF Core?
Eliminates boilerplate SQL code
Supports multiple databases (SQL Server, PostgreSQL, MySQL, SQLite, etc.)
Provides LINQ queries for expressive data access
Enables migrations for schema evolution
Integrates seamlessly with ASP.NET Core
Best Practices for EF Core Data Access
1. Use Dependency Injection for DbContext
Register DbContext in Program.cs:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
2. Keep DbContext Scoped
3. Use AsNoTracking for Read-Only Queries
var customers = _context.Customers.AsNoTracking().ToList();
This improves performance by disabling change tracking when updates aren’t needed.
4. Apply Migrations Instead of Manual SQL
dotnet ef migrations add InitialCreate
dotnet ef database update
This keeps schema changes version-controlled.
5. Use Repository & Unit of Work Patterns
Abstract data access logic to keep controllers clean.
6. Handle Exceptions Gracefully
Wrap database calls in try-catch blocks and log errors.
7. Avoid N+1 Query Problems
Use Include for eager loading:
var orders = _context.Orders.Include(o => o.Customer).ToList();
8. Use Async Methods
var customer = await _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
Async improves scalability for web applications.
9. Validate Data Before Saving
Use DataAnnotations or Fluent Validation to ensure data integrity.
10. Optimize Queries
Real-Time Example: Customer Management System
Step 1: Define the Model
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Step 2: Create the DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Customer> Customers { get; set; }
}
Step 3: Repository Pattern
public interface ICustomerRepository
{
Task<IEnumerable<Customer>> GetAllAsync();
Task<Customer> GetByIdAsync(int id);
Task AddAsync(Customer customer);
Task UpdateAsync(Customer customer);
Task DeleteAsync(int id);
}
public class CustomerRepository : ICustomerRepository
{
private readonly AppDbContext _context;
public CustomerRepository(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Customer>> GetAllAsync() =>
await _context.Customers.AsNoTracking().ToListAsync();
public async Task<Customer> GetByIdAsync(int id) =>
await _context.Customers.FindAsync(id);
public async Task AddAsync(Customer customer)
{
await _context.Customers.AddAsync(customer);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Customer customer)
{
_context.Customers.Update(customer);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var customer = await _context.Customers.FindAsync(id);
if (customer != null)
{
_context.Customers.Remove(customer);
await _context.SaveChangesAsync();
}
}
}
Step 4: Inject Repository into Controller
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly ICustomerRepository _repository;
public CustomersController(ICustomerRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await _repository.GetAllAsync());
[HttpPost]
public async Task<IActionResult> Add(Customer customer)
{
await _repository.AddAsync(customer);
return CreatedAtAction(nameof(GetAll), customer);
}
}
Example
A client app calls /api/customers → returns all customers.
A new customer is added via POST /api/customers.
EF Core handles database operations seamlessly, following best practices.
Conclusion
Entity Framework Core makes data access simpler, cleaner, and more maintainable. By following best practices like scoped DbContext, async queries, AsNoTracking, repository pattern, and migrations, you ensure your application is scalable, testable, and efficient.