ASP.NET Core, ASP.NET

Optimizing Performance in ASP.NET Core APIs: From Caching to Async DB Queries

Building high-performance APIs is critical in modern applications where scalability, responsiveness, and resource utilization directly impact user experience and business success. ASP.NET Core, a cross-platform and open-source framework, offers powerful tools and patterns to boost API performance. However, simply relying on default configurations can leave a lot of performance potential unused.

This blog explores optimization strategies in detail, combining theory with practical code examples. We’ll start with caching mechanisms that reduce unnecessary database hits and then move on to asynchronous programming with async await to handle concurrent workloads efficiently. After that, we will dig deeper into EF Core optimizations for leaner queries and examine how to offload heavy tasks into background jobs. Finally, we will conclude the content with logging and monitoring approaches that offer visibility into performance bottlenecks.

Whatever strategies we have discussed here are complementary techniques that together create an ecosystem of optimized performance in ASP.NET Core APIs. Whether developing a microservice or a large enterprise-grade API, implementing these practices will significantly improve throughput, responsiveness, and reliability.

Applying Caching Strategies in ASP.NET Core APIs

Caching is one of the most effective performance improvement methods in ASP.NET Core APIs. By storing frequently accessed data in memory or distributed storage, caching reduces repeated calls to expensive resources like databases or external APIs. This leads to faster response times, reduced server load, and improved scalability. There are several caching tactics available in ASP.NET Core:

  • In-Memory Caching: It stores data in the memory of the web server, which is highly suitable for lightweight, frequently reused data in single-server deployments. The only downside of in-memory caching is its limited scalability in distributed environments.
  • Distributed Caching: Being most appropriate for cloud or multi-server deployments, distributed caching uses providers like Redis or SQL Server to store cache centrally. This ensures consistency across multiple instances.
  • Response Caching: It allows entire HTTP responses to be cached, reducing the need to recompute results for similar requests.
  • Cache Tagging and Expiration Policies: ASP.NET Core supports absolute and sliding expirations to ensure cache consistency and prevent obsolete data issues. Just to let you know, absolute expiration refers to the cache that will expire after a specific time, regardless of whether it is used in that time span or not. On the other hand, sliding expiration is all about the cache that will expire after a specific time only if it has not been used in the given time period.

Here’s a quick example of in-memory caching in ASP.NET Core:

// In-memory caching example 

public class ProductsController : ControllerBase 

{ 

 private readonly IMemoryCache _cache; 

 private readonly AppDbContext _context; 

 public ProductsController(IMemoryCache cache, AppDbContext context)  { 

 _cache = cache; 

 _context = context; 

 } 

 [HttpGet("{id}")] 

 public async Task<IActionResult> GetProduct(int id) 

 { 

 if (!_cache.TryGetValue(id, out Product product))

 { 

 product = await _context.Products.FindAsync(id); 

 var cacheOptions = new MemoryCacheEntryOptions() 

 .SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); 

 _cache.Set(id, product, cacheOptions); 

 } 

 return Ok(product); 

 } 

}

Implementing Asynchronous Programming with Async Await

ASP.NET Core APIs are naturally well-suited for asynchronous programming, which helps improve scalability and responsiveness under heavy load. This type of programming allows multiple tasks to execute without waiting for each other to complete. It enhances application responsiveness and efficiency by not interrupting the main program flow for time-consuming operations like network requests or file input/output.

In asynchronous programming, a program can begin a long-running task and continue working on other things to return to the original task only when it is fully complete. This is where it differs from synchronous programming, where tasks execute one by one in a fixed pattern, forcing the program to pause and wait for each task to complete.

Here’s an example of an asynchronous controller action using async/await: 

// Asynchronous controller action 

[HttpGet("products")] 

public async Task<IActionResult> GetProducts() 

{ 

 var products = await _context.Products.ToListAsync(); 

 return Ok(products); 

}

Optimizing EF Core to Make APIs Responsive

Entity Framework Core (EF Core) provides a high-level abstraction for working with databases in .NET. But if it is not used carefully, it can introduce performance bottlenecks. Optimizing EF Core queries and configurations is important for achieving responsive APIs. Some important techniques include:

  • Use AsNoTracking for Read-Only Queries: Disables EF Core’s change tracking mechanism for queries that don’t require updates, reducing overhead.
  • Efficient Projections: Instead of loading entire entities, project only the required fields using `Select`.
  • Batching and Pagination: Avoid loading large datasets at once. Use `Skip` and `Take` to fetch only the necessary subset of data.
  • Compiled Queries: Pre-compile repeatedly used queries for quicker execution.
  • Connection Pooling and Indexing: Ensure the database itself is optimized with proper indexing and leverage connection pooling in EF Core.

Below is an example of using AsNoTracking in a query: 

// Using AsNoTracking for read-only queries 

var products = await _context.Products 

 .AsNoTracking() 

 .Where(p => p.IsActive) 

 .ToListAsync();

Offloading Work with Background Jobs

Not all tasks need to run synchronously as part of an API request. Offloading resource-intensive or long-running operations to background jobs can free up API threads and return faster responses to clients. Common use cases include sending emails, processing images, generating reports, or performing integrations with external systems. ASP.NET Core integrates well with background job libraries like Hangfire or Quartz.NET. Apart from that, it also supports hosted services for simpler scenarios.

Here’s an example of using a background service in ASP.NET Core: 

// Background service example 

public class EmailBackgroundService : BackgroundService 

{ 

 private readonly IEmailSender _emailSender; 

 public EmailBackgroundService(IEmailSender emailSender) 

 { 

 _emailSender = emailSender; 

 } 

 protected override async Task ExecuteAsync(CancellationToken stoppingToken)  { 

 while (!stoppingToken.IsCancellationRequested) 

 { 

 await _emailSender.SendPendingEmailsAsync(); 

 await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 

 } 

 } 

}

Tracking API Behavior Through Logging and Monitoring

Optimization doesn’t stop at coding best practices. Visibility into API behavior is essential for identifying and fixing bottlenecks. And API testing is the best way to make sure that the API behavior is robust, secure, and as expected. Logging and monitoring offer great visibility into API behavior. ASP.NET Core has built-in support for structured logging via providers like Serilog, NLog, or the default Microsoft.Extensions.Logging. Monitoring solutions such as Application Insights, Prometheus, or Elastic Stack help track performance metrics, error rates, and request throughput. Key strategies include:

  • Structured Logging: Use structured formats (like JSON) to make logs machine-readable and easy to query.
  • Centralized Logging: Aggregate logs from multiple services into a single store for analysis.
  • Performance Metrics: Track response times, memory usage, CPU utilization, and database query performance.
  • Alerting: Proactively identify issues with limit-based alerts on performance metrics.

Here’s an example of configuring Serilog ASP.NET Core: 

// Configuring Serilog 

public class Program 

{ 

 public static void Main(string[] args) 

 { 

 Log.Logger = new LoggerConfiguration() 

 .WriteTo.Console() 

 .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)  .CreateLogger(); 

 CreateHostBuilder(args).Build().Run(); 

 } 

 public static IHostBuilder CreateHostBuilder(string[] args) => 

 Host.CreateDefaultBuilder(args)

 .UseSerilog() 

 .ConfigureWebHostDefaults(webBuilder => 

 { 

 webBuilder.UseStartup<Startup>(); 

 }); 

}

Final Thoughts

Performance optimization in ASP.NET Core APIs is not about a single silver bullet but rather a layered approach that combines caching, asynchronous programming, database query optimization, background processing, and robust logging/monitoring. Each of these strategies handles a different area of performance, and when combined, they result in APIs that are faster, more scalable, and more reliable. In some cases, developers have increased the potential of ASP.NET Core Web API from managing 1370 RPS to a staggering 25,798 RPS. Thus, by applying these practices, developers can build APIs that not only meet functional requirements but also deliver exceptional user experiences under demanding conditions.

In case you are looking for a tech partner for a long time that can improve the performance of your mobile application, it is advisable to book a consultation call with experts at InnovationM right away.

Leave a Reply