Why is DbContext Scoped?
DbContext is typically registered as Scoped because it is designed to handle a single unit of work, usually one HTTP request.
builder.Services.AddDbContext<AppDbContext>();
By default, AddDbContext() registers it as Scoped.
Why Scoped?
For each request:
A new DbContext is created.
All repositories/services in that request share the same context.
Changes are tracked consistently.
The context is disposed when the request ends.
Problems with Singleton DbContext
services.AddSingleton<AppDbContext>();
Bad because:
DbContext is not thread-safe.
Multiple requests may use the same instance simultaneously.
Change Tracking grows indefinitely.
Memory leaks can occur.
Data inconsistencies become likely.
Problems with Transient DbContext
services.AddTransient<AppDbContext>();
Each injection gets a new instance.
RepositoryA -> DbContext A
RepositoryB -> DbContext B
A single business transaction may use multiple contexts, making transactions and tracking difficult.
InShort
DbContext is registered as Scoped because one database context should generally serve one request or unit of work. This ensures proper change tracking, transaction management, and thread safety while avoiding the problems associated with Singleton or excessive context creation from Transient lifetimes.
Why shouldn't Scoped be injected into Singleton?
Example
services.AddScoped<IUserService, UserService>();
services.AddSingleton<ReportGenerator>();
public class ReportGenerator
{
public ReportGenerator(IUserService userService)
{
}
}
This causes a lifetime mismatch.
Why?
A Singleton is created once:
Application Start
|
+-- Singleton Created
A Scoped service exists per request:
Request 1 → UserService Instance A
Request 2 → UserService Instance B
The Singleton would hold a reference to the first scoped instance forever.
Problems
Stale data.
Memory leaks.
Unexpected behavior.
Runtime DI exceptions.
ASP.NET Core often throws:
Cannot consume scoped service from singleton
Correct Approach
Inject:
or
and create a scope when needed.
InShort
A Scoped service has a shorter lifetime than a Singleton. Injecting a Scoped dependency into a Singleton can cause stale references, thread-safety issues, and runtime errors because the Singleton may outlive the Scoped object.
Why can caching improve performance AND create bugs?
Caching stores frequently accessed data in memory so it can be reused instead of repeatedly fetching it from a database or external service.
Performance Improvement
Without cache:
Request
↓
Database
↓
Response
Every request hits the database.
With cache:
Request
↓
Cache
↓
Response
Benefits:
Faster response times.
Reduced database load.
Lower network latency.
Better scalability.
Example:
_memoryCache.GetOrCreate(
"Products",
entry =>
{
return repository.GetProducts();
});
How Bugs Happen
Stale Data
Database:
Product Price = 100
Cache:
Product Price = 100
Database updated:
Product Price = 120
Cache still returns:
100
Users see incorrect data.
Cache Invalidation Problems
One of the hardest problems in software:
When should cache be refreshed?
Too long:
Too short:
Distributed Cache Issues
In multiple servers:
Server A Cache = Updated
Server B Cache = Old
Users may receive inconsistent responses.
InShort
Caching improves performance by reducing expensive operations such as database queries and API calls. However, it can introduce bugs through stale data, invalidation issues, synchronization problems, and inconsistent state across multiple servers.
Why does an API become slow in production?
An API that is fast locally may become slow under real-world load.
1. Database Bottlenecks
Most common reason.
var orders = _context.Orders.ToList();
Poor indexing or inefficient queries can dramatically increase response times.
2. N+1 Query Problem
foreach(var order in orders)
{
var customer = order.Customer;
}
One query becomes hundreds of queries.
3. External Service Calls
Payment API
Email API
Third-party API
Even if your API is fast, waiting on external systems slows everything down.
4. Excessive Logging
_logger.LogInformation(...);
Huge amounts of logging can impact performance.
5. Thread Pool Starvation
Blocking calls:
Thread.Sleep()
.Result
.Wait()
consume worker threads and reduce throughput.
6. Memory Pressure
Large objects:
List<Customer> customers = GetMillions();
cause:
High GC activity.
Increased CPU usage.
Slower response times.
7. Network Latency
Production systems involve:
Client
↓
Load Balancer
↓
API
↓
Database
Each hop adds latency.
InShort
APIs usually become slow in production due to database bottlenecks, N+1 queries, external service dependencies, excessive logging, memory pressure, thread pool starvation, high traffic, or network latency. Profiling and monitoring are essential to identify the real bottleneck.
Why does async improve scalability?
Many people think async makes code faster.
Important
Async doesn't necessarily make an operation faster.
It makes better use of server resources.
Synchronous Request
public string GetData()
{
var data = apiClient.GetData();
return data;
}
Thread waits:
Thread 1
|
| Waiting...
| Waiting...
| Waiting...
During waiting, the thread cannot serve other requests.
Asynchronous Request
public async Task<string> GetData()
{
return await apiClient.GetDataAsync();
}
Thread starts work
↓
I/O operation starts
↓
Thread returned to pool
↓
I/O completes
↓
Thread resumes execution
The thread is free to handle other requests while waiting.
Example
Suppose:
Synchronous
Many threads remain blocked.
100 Threads Busy
Asynchronous
Threads are released during I/O waits.
10-20 Threads handling
100 Requests
Much better scalability.
InShort
Async improves scalability because it releases threads while waiting for I/O operations such as database calls, file access, or HTTP requests. This allows the server to handle more concurrent requests with the same hardware resources. Async improves throughput and resource utilization, though it does not necessarily make individual operations execute faster.
Summary
Understanding why DbContext is registered as Scoped, avoiding lifetime mismatches in dependency injection, using caching carefully, identifying production performance bottlenecks, and leveraging asynchronous programming appropriately are fundamental concepts for building scalable and reliable ASP.NET Core applications. These practices help improve performance, maintain thread safety, optimize resource utilization, and reduce common issues in production environments.