When I first started using EF Core inside an ASP.NET Core project, I honestly didn’t think too much about dependency injection. I just wanted to run queries, get data, and move on. But after a few projects and some mistakes along the way. I realized that getting EF Core to play nicely with dependency injection (DI) was not just “a best practice”, but something that saved me headaches later on.
This post is my attempt to share what I learned. I’ll keep it casual, like I’m talking to a friend over coffee. Don’t expect textbook-style precision, but hopefully you’ll see how DI and EF Core can fit together in a way that feels natural and maintainable.
Starting Simple: Creating the DbContext
In my first project, I remember I just created a DbContext class without thinking about DI. It looked something like this:
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
}This part wasn’t too hard. The DbSet properties were straightforward each one represented a table. But the real question was: how do I use this in my controllers or services without creating new AppDbContext() everywhere?
That’s where DI came in.
Hooking Up DbContext in Program.cs
Back in the old days (like .NET Core 2), we had Startup.cs where we configured services. Now with .NET 9, it’s all inside Program.cs.
I usually do it like this:
var builder = WebApplication.CreateBuilder(args);
// Add DbContext with dependency injection
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();The key thing is AddDbContext. This tells ASP.NET Core that whenever someone asks for AppDbContext, DI will provide an instance that’s properly configured with SQL Server.
In my early days, I used to forget the connection string part, and then I’d spend an hour wondering why migrations weren’t working. Lesson learned: always double-check your appsettings.json.
Why Dependency Injection Matters
I remember in one project, we had multiple services that needed to query the same data. Without DI, I would have ended up writing something like:
var context = new AppDbContext(...);everywhere. That meant hardcoding connection strings or reusing options manually. It quickly turned messy.
With DI, I just injected AppDbContext into my services:
public class CustomerService
{
private readonly AppDbContext _dbContext;
public CustomerService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Customer>> GetActiveCustomersAsync()
{
return await _dbContext.Customers
.Where(c => c.IsActive)
.ToListAsync();
}
}Now the context was managed by the framework, and I didn’t worry about disposing it, connection pooling, or lifecycle issues. That was a big relief.
Scoped, Transient, Singleton — Which One?
At some point, I got curious (or maybe confused). Should my DbContext be Singleton, Transient, or Scoped?
The short answer: Scoped.
EF Core’s DbContext is not thread-safe, so you don’t want a single instance floating around forever. If you make it Transient, you might create too many unnecessary instances. Scoped is the sweet spot: one context per request.
And honestly, I learned this lesson the hard way when I accidentally registered it as Singleton in one project. We had weird issues where queries would mix data from previous requests. That was a nightmare. Since then, I just let AddDbContext handle it by default, it’s Scoped.
Using DbContext in Controllers
Here’s a very typical example. Imagine we’re building an API for customers:
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly AppDbContext _dbContext;
public CustomersController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
public async Task<IActionResult> GetCustomers()
{
var customers = await _dbContext.Customers.ToListAsync();
return Ok(customers);
}
}This is as clean as it gets. I didn’t have to new up the context. DI did the work for me. And if I need to write unit tests later, I can swap AppDbContext with an in-memory provider.
Joins: Bringing Related Data Together
In real business scenarios, you almost never just query a single table. I remember working on an invoicing system where I needed to pull invoice details together with payment info. That’s when joins came in handy.
var invoicePayments = await _dbContext.Invoices
.Join(_dbContext.Payments,
invoice => invoice.Id,
payment => payment.InvoiceId,
(invoice, payment) => new
{
InvoiceNumber = invoice.Number,
PaymentAmount = payment.Amount,
PaymentDate = payment.Date
})
.ToListAsync();At first, I was writing raw SQL for this kind of thing. But later I realized LINQ was powerful enough. Plus, since I was still using DI for DbContext, testing this code with different providers (like InMemoryDatabase) became easy.
Filtering and Projections
Here’s another real-world case. In one system, we had to filter out inactive users and only return lightweight projections. Instead of returning the entire entity, I shaped the result:
var activeUsers = await _dbContext.Users
.Where(u => u.IsActive)
.Select(u => new UserDto
{
Id = u.Id,
Name = u.FullName,
Email = u.Email
})
.ToListAsync();This saved bandwidth because I wasn’t serializing entire objects. And since UserDto is just a C# class, I could easily customize it for the API.
Adding Services on Top of DbContext
In bigger projects, I try not to dump all queries in the controller. I create services that depend on AppDbContext. For example:
public class OrderService
{
private readonly AppDbContext _dbContext;
public OrderService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<OrderDto>> GetOrdersByCustomerAsync(int customerId)
{
return await _dbContext.Orders
.Where(o => o.CustomerId == customerId)
.Select(o => new OrderDto
{
Id = o.Id,
Total = o.TotalAmount,
Created = o.CreatedDate
})
.ToListAsync();
}
}And then in Program.cs:
builder.Services.AddScoped<OrderService>();This made the controllers cleaner and gave me a nice separation of concerns.
One Gotcha: Lifetime Mismatches
There was one situation where I accidentally injected DbContext into a Singleton service. That blew up quickly. You can’t mix a Scoped dependency (DbContext) inside a Singleton.
So whenever you register services, just keep in mind: if it depends on DbContext, make it Scoped too.
Wrapping Up
Looking back, I can say that using EF Core with Dependency Injection has made my code cleaner, more testable, and much easier to maintain. The first time I set it up, it felt like extra work. But once you get used to the flow registering DbContext, injecting it into services, and keeping things Scoped it really becomes second nature.
I still remember the frustration of messing up lifetimes or forgetting to configure the connection string. But those mistakes were the best teachers.
If you’re starting fresh, my advice is: keep it simple at first. Add your DbContext, inject it into one controller, and just try a basic query. Once that works, you’ll see how naturally it fits into ASP.NET Core’s DI system.






