11 ASP.NET Core Best Practices (with Examples)
Learn 11 ASP.NET Core best practices with examples: async programming, memory management, HttpClientFactory, efficient middleware, and more to boost performance and scalability.
Introduction
Building a web app with ASP.NET Core is more than just writing endpoints and views — the real test is how your app behaves under load: how quickly it responds, how many users it can serve, and how stable it stays when things go wrong.
Too often, developers assume “if it works, it’s good enough.” In reality, small inefficiencies — blocking threads, unnecessary memory allocations, or unoptimized database queries — can snowball into slowdowns, high resource usage, or even crashes.
For example:
- The .NET runtime places objects of 85,000 bytes or more into the Large Object Heap (LOH). LOH cleanup only happens during full (generation 2) garbage collections, which are expensive and pause execution (Microsoft Learn).
- Blocking calls (
.Result,.Wait()) in ASP.NET Core can cause thread pool starvation. Threads get tied up waiting, leaving fewer available for new requests. Microsoft explicitly warns against blocking calls in hot code paths (Microsoft Learn).
The good news? These issues are avoidable. Below are 11 best practices, with examples, to help you build apps that are faster, more scalable, and production-ready.
1. Use Asynchronous APIs
When you block on I/O, you waste threads. Always use async/await for database, HTTP, and file calls.
❌ Blocking example:
public IActionResult GetData()
{
var result = _service.GetDataAsync().Result; // Blocks thread
return Ok(result);
}
✅ Async example:
public async Task<IActionResult> GetData()
{
var result = await _service.GetDataAsync();
return Ok(result);
}
2. Avoid Returning Huge Lists
Don’t dump entire tables into memory. Use paging or streaming.
❌ Bad:
public async Task<IActionResult> GetUsers()
{
return Ok(await _db.Users.ToListAsync());
}
✅ Better (with paging):
public async Task<IActionResult> GetUsers(int page = 1, int pageSize = 50)
{
var users = await _db.Users
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(users);
}
3. Reuse Large Objects
Large arrays go into the LOH. Instead of recreating them every time, reuse with pooling.
✅ Using ArrayPool:
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
// use buffer...
pool.Return(buffer);
4. Optimize Data Access
Load only what you need.
❌ Bad:
var users = await _db.Users.ToListAsync();
✅ Better:
var usernames = await _db.Users
.Select(u => u.Username)
.ToListAsync();
For read-only queries:
var users = await _db.Users
.AsNoTracking()
.ToListAsync();
5. Use HttpClientFactory
Creating new HttpClient instances exhausts sockets. Use HttpClientFactory instead.
✅ Good:
public class MyService
{
private readonly HttpClient _client;
public MyService(HttpClient client)
{
_client = client;
}
public Task<string> GetDataAsync() =>
_client.GetStringAsync("https://api.example.com/data");
}
Registered in Program.cs:
builder.Services.AddHttpClient<MyService>();
6. Keep Middleware Lean
Every request runs through middleware, so keep it efficient.
✅ Example:
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next();
stopwatch.Stop();
Console.WriteLine($"Request took {stopwatch.ElapsedMilliseconds}ms");
});
7. Offload Long Tasks
Don’t block user requests with heavy tasks. Run them in the background.
✅ BackgroundService example:
public class EmailQueueService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var email = await _queue.GetNextAsync();
await _emailSender.SendAsync(email);
}
}
}
8. Minify & Compress Responses
Reduce payload size with compression.
builder.Services.AddResponseCompression();
app.UseResponseCompression();
9. Use the Latest Versions
Each new .NET release comes with performance improvements (e.g., .NET 8 improved JSON serialization and async handling). Stay updated to benefit from these gains.
10. Use Exceptions Correctly
Exceptions should not be your logic flow.
❌ Bad:
try
{
var user = await _db.Users.FirstAsync(u => u.Id == id);
}
catch
{
return NotFound();
}
✅ Better:
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
if (user == null) return NotFound();
11. Be Careful with HttpContext
Don’t pass HttpContext across threads or store it for later use.
❌ Bad:
Task.Run(() =>
{
var user = context.User.Identity.Name; // risky
});
✅ Better:
var user = context.User.Identity.Name;
// Pass the value, not HttpContext itself
Conclusion
Making your ASP.NET Core app production-ready isn’t just about “making it work” — it’s about making it work well under real-world conditions. The practices above — async programming, smart memory usage, efficient data access, lean middleware, and proper resource handling — are based on how the .NET runtime works and what typically breaks in production.
Key Takeaways:
- Measure before optimizing: Use profiling tools (PerfView, Visual Studio diagnostics).
- Optimize hot paths first: Middleware, logging, and authentication run on every request.
- Balance performance and maintainability: Don’t over-engineer unless the gains are real.
- Stay updated: New .NET releases bring performance and security improvements.
If your business needs help applying these principles in a real-world project, our team at InSync Software can help. We specialize in building tailored solutions on ASP.NET Core that are fast, scalable, and designed around your business goals.
References & Further Reading
- Asynchronous Programming with Async and Await in ASP.NET Core (Code Maze)
- ASP.NET Core Best Practices — Microsoft Learn
- Large Object Heap (LOH) — .NET Documentation
👉 Your turn: Which of these best practices do you already follow in your ASP.NET Core projects? Share your thoughts in the comments — I’d love to hear how you optimize your apps.