Optimize ASP.NET Core performance: in-memory cache, Redis distributed cache, response/output caching, and performance patterns.
Performance in ASP.NET Core is achieved through caching, response compression, output caching, and mindful async patterns. In-memory caching works for single-server scenarios; distributed caching with Redis scales across multiple instances; response caching reduces redundant processing for public endpoints.
IMemoryCache stores data in process memory with expiration policies:
1// Register memory cache
2builder.Services.AddMemoryCache();
3
4// Using IMemoryCache
5public class ProductService
6{
7 private readonly IMemoryCache _cache;
8 private readonly IProductRepository _repository;
9 private static readonly string CacheKeyPrefix = "product_";
10
11 public ProductService(IMemoryCache cache, IProductRepository repository)
12 {
13 _cache = cache;
14 _repository = repository;
15 }
16
17 public async Task<Product?> GetByIdAsync(int id)
18 {
19 var cacheKey = $"{CacheKeyPrefix}{id}";
20
21 // Try to get from cache
22 if (_cache.TryGetValue(cacheKey, out Product? product))
23 {
24 return product;
25 }
26
27 // Get from database
28 product = await _repository.GetByIdAsync(id);
29
30 if (product != null)
31 {
32 // Cache with options
33 var cacheOptions = new MemoryCacheEntryOptions()
34 .SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
35 .SetSlidingExpiration(TimeSpan.FromMinutes(2))
36 .SetPriority(CacheItemPriority.Normal)
37 .SetSize(1); // For size-limited cache
38
39 _cache.Set(cacheKey, product, cacheOptions);
40 }
41
42 return product;
43 }
44
45 // GetOrCreate pattern
46 public async Task<List<Product>> GetAllAsync()
47 {
48 return await _cache.GetOrCreateAsync("all_products", async entry =>
49 {
50 entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
51 entry.SlidingExpiration = TimeSpan.FromMinutes(1);
52
53 return await _repository.GetAllAsync();
54 }) ?? new List<Product>();
55 }
56
57 // Invalidate cache
58 public async Task UpdateAsync(Product product)
59 {
60 await _repository.UpdateAsync(product);
61
62 // Remove specific item
63 _cache.Remove($"{CacheKeyPrefix}{product.Id}");
64
65 // Remove list cache
66 _cache.Remove("all_products");
67 }
68}
69
70// Configure cache size limits
71builder.Services.AddMemoryCache(options =>
72{
73 options.SizeLimit = 1024; // 1024 units
74 options.CompactionPercentage = 0.25; // Remove 25% when full
75});Redis provides shared, persistent caching across multiple server instances:
1// Install: Microsoft.Extensions.Caching.StackExchangeRedis
2
3// Configure Redis distributed cache
4builder.Services.AddStackExchangeRedisCache(options =>
5{
6 options.Configuration = builder.Configuration.GetConnectionString("Redis");
7 options.InstanceName = "MyApp_";
8});
9
10// Using IDistributedCache
11public class CachedUserService
12{
13 private readonly IDistributedCache _cache;
14 private readonly IUserRepository _repository;
15 private readonly ILogger<CachedUserService> _logger;
16
17 public CachedUserService(
18 IDistributedCache cache,
19 IUserRepository repository,
20 ILogger<CachedUserService> logger)
21 {
22 _cache = cache;
23 _repository = repository;
24 _logger = logger;
25 }
26
27 public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
28 {
29 var cacheKey = $"user:{id}";
30
31 // Try to get from cache
32 var cached = await _cache.GetStringAsync(cacheKey, ct);
33 if (cached != null)
34 {
35 _logger.LogDebug("Cache HIT for user {UserId}", id);
36 return JsonSerializer.Deserialize<User>(cached);
37 }
38
39 _logger.LogDebug("Cache MISS for user {UserId}", id);
40
41 // Get from database
42 var user = await _repository.GetByIdAsync(id, ct);
43
44 if (user != null)
45 {
46 var options = new DistributedCacheEntryOptions
47 {
48 AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
49 SlidingExpiration = TimeSpan.FromMinutes(10)
50 };
51
52 await _cache.SetStringAsync(
53 cacheKey,
54 JsonSerializer.Serialize(user),
55 options,
56 ct);
57 }
58
59 return user;
60 }
61
62 // Cache-aside pattern with extension
63 public async Task<T?> GetOrSetAsync<T>(
64 string key,
65 Func<Task<T?>> factory,
66 DistributedCacheEntryOptions? options = null,
67 CancellationToken ct = default) where T : class
68 {
69 var cached = await _cache.GetStringAsync(key, ct);
70 if (cached != null)
71 {
72 return JsonSerializer.Deserialize<T>(cached);
73 }
74
75 var value = await factory();
76 if (value != null)
77 {
78 options ??= new DistributedCacheEntryOptions
79 {
80 AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
81 };
82
83 await _cache.SetStringAsync(key, JsonSerializer.Serialize(value), options, ct);
84 }
85
86 return value;
87 }
88
89 // Refresh cache
90 public async Task RefreshAsync(string key, CancellationToken ct = default)
91 {
92 await _cache.RefreshAsync(key, ct);
93 }
94
95 // Invalidate cache
96 public async Task InvalidateAsync(int userId, CancellationToken ct = default)
97 {
98 await _cache.RemoveAsync($"user:{userId}", ct);
99 }
100}Cache HTTP responses at the pipeline level for public endpoints:
1// Enable response caching
2builder.Services.AddResponseCaching();
3app.UseResponseCaching();
4
5// Controller with response caching
6[ApiController]
7[Route("api/[controller]")]
8public class ProductsController : ControllerBase
9{
10 // Cache for 60 seconds
11 [HttpGet]
12 [ResponseCache(Duration = 60)]
13 public async Task<IActionResult> GetAll()
14 {
15 var products = await _service.GetAllAsync();
16 return Ok(products);
17 }
18
19 // Cache with Vary header
20 [HttpGet("category/{category}")]
21 [ResponseCache(Duration = 120, VaryByQueryKeys = new[] { "page", "pageSize" })]
22 public async Task<IActionResult> GetByCategory(string category, int page = 1, int pageSize = 10)
23 {
24 var products = await _service.GetByCategoryAsync(category, page, pageSize);
25 return Ok(products);
26 }
27
28 // No caching for user-specific data
29 [HttpGet("my-orders")]
30 [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
31 public async Task<IActionResult> GetMyOrders()
32 {
33 var orders = await _orderService.GetUserOrdersAsync(User.GetUserId());
34 return Ok(orders);
35 }
36}
37
38// Cache profiles
39builder.Services.AddControllers(options =>
40{
41 options.CacheProfiles.Add("Default", new CacheProfile
42 {
43 Duration = 30,
44 Location = ResponseCacheLocation.Any
45 });
46
47 options.CacheProfiles.Add("Long", new CacheProfile
48 {
49 Duration = 3600, // 1 hour
50 Location = ResponseCacheLocation.Any
51 });
52
53 options.CacheProfiles.Add("Private", new CacheProfile
54 {
55 Duration = 300, // 5 minutes
56 Location = ResponseCacheLocation.Client
57 });
58});
59
60// Use profile
61[ResponseCache(CacheProfileName = "Default")]
62public async Task<IActionResult> GetPopular()
63{
64 return Ok(await _service.GetPopularAsync());
65}
66
67// Output caching (ASP.NET Core 7+)
68builder.Services.AddOutputCache(options =>
69{
70 options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(1)));
71
72 options.AddPolicy("Products", builder =>
73 builder.Expire(TimeSpan.FromMinutes(5))
74 .Tag("products"));
75});
76
77app.UseOutputCache();
78
79// Use output cache
80[OutputCache(PolicyName = "Products")]
81public async Task<IActionResult> GetProducts()
82{
83 return Ok(await _service.GetAllAsync());
84}
85
86// Invalidate output cache by tag
87await _outputCache.EvictByTagAsync("products", ct);Additional performance patterns for high-throughput ASP.NET Core:
1// 1. Response compression
2builder.Services.AddResponseCompression(options =>
3{
4 options.EnableForHttps = true;
5 options.Providers.Add<BrotliCompressionProvider>();
6 options.Providers.Add<GzipCompressionProvider>();
7 options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
8 new[] { "application/json", "text/plain" });
9});
10
11builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
12{
13 options.Level = CompressionLevel.Fastest;
14});
15
16app.UseResponseCompression();
17
18// 2. Async streaming for large data
19[HttpGet("export")]
20public async IAsyncEnumerable<ProductDto> ExportProducts(
21 [EnumeratorCancellation] CancellationToken ct)
22{
23 await foreach (var product in _repository.StreamAllAsync(ct))
24 {
25 yield return product.ToDto();
26 }
27}
28
29// 3. Pagination and limiting
30[HttpGet]
31public async Task<ActionResult<PagedResult<Product>>> GetProducts(
32 [FromQuery] int page = 1,
33 [FromQuery] int pageSize = 20)
34{
35 pageSize = Math.Min(pageSize, 100); // Limit max page size
36
37 var (items, total) = await _service.GetPagedAsync(page, pageSize);
38
39 return Ok(new PagedResult<Product>
40 {
41 Items = items,
42 Page = page,
43 PageSize = pageSize,
44 TotalCount = total,
45 TotalPages = (int)Math.Ceiling(total / (double)pageSize)
46 });
47}
48
49// 4. Database query optimization
50public async Task<List<ProductDto>> GetOptimizedAsync()
51{
52 return await _context.Products
53 .AsNoTracking() // No change tracking for read-only
54 .Where(p => p.IsActive)
55 .Select(p => new ProductDto // Project only needed fields
56 {
57 Id = p.Id,
58 Name = p.Name,
59 Price = p.Price
60 })
61 .Take(100) // Limit results
62 .ToListAsync();
63}
64
65// 5. Parallel processing
66public async Task<DashboardData> GetDashboardAsync()
67{
68 var ordersTask = _orderService.GetRecentAsync();
69 var productsTask = _productService.GetTopSellingAsync();
70 var statsTask = _statsService.GetSummaryAsync();
71
72 await Task.WhenAll(ordersTask, productsTask, statsTask);
73
74 return new DashboardData
75 {
76 RecentOrders = await ordersTask,
77 TopProducts = await productsTask,
78 Stats = await statsTask
79 };
80}
81
82// 6. Object pooling
83builder.Services.AddSingleton<ObjectPool<StringBuilder>>(
84 new DefaultObjectPoolProvider().CreateStringBuilderPool());
85
86public class ReportGenerator
87{
88 private readonly ObjectPool<StringBuilder> _pool;
89
90 public string GenerateReport(List<Order> orders)
91 {
92 var sb = _pool.Get();
93 try
94 {
95 foreach (var order in orders)
96 {
97 sb.AppendLine($"Order {order.Id}: {order.Total:C}");
98 }
99 return sb.ToString();
100 }
101 finally
102 {
103 _pool.Return(sb);
104 }
105 }
106}Your progress is saved in this browser, on this device only.