Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Asp.Net Core

.NET Core: Use IMemoryCache for Lightning-Fast Data Access Without Redis

- 03.02.26 - ErcanOPAK

Hitting database for same data 1000 times per second? IMemoryCache stores frequently-accessed data in RAM for instant access.

Setup:

// Program.cs or Startup.cs
builder.Services.AddMemoryCache();

Basic Usage:

public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly ApplicationDbContext _db;

    public ProductService(IMemoryCache cache, ApplicationDbContext db)
    {
        _cache = cache;
        _db = db;
    }

    public async Task GetProductAsync(int id)
    {
        // Try get from cache
        if (_cache.TryGetValue($"product_{id}", out Product product))
        {
            return product;  // Cache hit - instant return!
        }

        // Cache miss - query database
        product = await _db.Products.FindAsync(id);

        // Store in cache for 10 minutes
        _cache.Set($"product_{id}", product, TimeSpan.FromMinutes(10));

        return product;
    }
}

// First call: 50ms (database query)
// Next 1000 calls: 0.1ms each (from cache)
// = 500x faster!

Advanced – Get or Create Pattern:

public async Task> GetProductsByCategoryAsync(int categoryId)
{
    return await _cache.GetOrCreateAsync($"products_category_{categoryId}", async entry =>
    {
        // Set cache options
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15);
        entry.SlidingExpiration = TimeSpan.FromMinutes(5);
        entry.Priority = CacheItemPriority.Normal;

        // This only runs if cache is empty
        return await _db.Products
            .Where(p => p.CategoryId == categoryId)
            .ToListAsync();
    });
}

// Clean syntax - cache management handled automatically

Cache Expiration Strategies:

// Absolute Expiration: Cache expires after fixed time
_cache.Set("key", value, new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
    // Expires 30 minutes from now, regardless of usage
});

// Sliding Expiration: Resets on every access
_cache.Set("key", value, new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(10)
    // Expires 10 minutes after LAST access
    // If accessed again, timer resets
});

// Combine both: Max lifetime with sliding
_cache.Set("key", value, new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),  // Max 1 hour
    SlidingExpiration = TimeSpan.FromMinutes(10)              // 10 min idle timeout
    // Expires after 1 hour OR 10 minutes of inactivity
});

// Size Limit: Prevent cache from growing too large
_cache.Set("key", value, new MemoryCacheEntryOptions
{
    Size = 1,  // This item counts as size 1
    Priority = CacheItemPriority.Low  // Evict this first if memory low
});

Cache Invalidation on Update:

public async Task UpdateProductAsync(Product product)
{
    await _db.SaveChangesAsync();

    // Remove from cache so next access gets fresh data
    _cache.Remove($"product_{product.Id}");

    // Also remove list caches containing this product
    _cache.Remove($"products_category_{product.CategoryId}");
}

// Cache stays in sync with database

Callback on Eviction:

_cache.Set("important_data", value, new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
    PostEvictionCallbacks =
    {
        new PostEvictionCallbackRegistration
        {
            EvictionCallback = (key, value, reason, state) =>
            {
                _logger.LogInformation($"Cache key {key} evicted. Reason: {reason}");

                if (reason == EvictionReason.Expired)
                {
                    // Refresh cache proactively
                    RefreshCache((string)key);
                }
            }
        }
    }
});

Performance Comparison – API Endpoint:

// Without cache:
[HttpGet("products/{id}")]
public async Task GetProduct(int id)
{
    var product = await _db.Products.FindAsync(id);
    return Ok(product);
}
// Load test: 100 requests/sec
// Average response: 50ms
// Database load: 100 queries/sec

// With cache:
[HttpGet("products/{id}")]
public async Task GetProduct(int id)
{
    var product = await _cache.GetOrCreateAsync($"product_{id}", async entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        return await _db.Products.FindAsync(id);
    });
    
    return Ok(product);
}
// Load test: 100 requests/sec
// Average response: 2ms (25x faster!)
// Database load: 0.1 queries/sec (first load only)

Cache Size Management:

// Configure size limit
builder.Services.AddMemoryCache(options =>
{
    options.SizeLimit = 1024;  // Total size limit
    options.CompactionPercentage = 0.2;  // Remove 20% when limit hit
});

// Specify size for each entry
_cache.Set("large_object", data, new MemoryCacheEntryOptions
{
    Size = 100  // This item counts as 100 units
});

_cache.Set("small_object", data, new MemoryCacheEntryOptions
{
    Size = 1
});

// When total size exceeds 1024, lowest priority items evicted first

Distributed Cache Alternative (Redis):

// IMemoryCache: In-process, lost on restart
// IDistributedCache: Shared across servers, persists

// For single server: Use IMemoryCache (faster)
// For multiple servers: Use IDistributedCache (Redis, SQL Server)

// Setup Redis distributed cache:
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});

// Usage is similar:
private readonly IDistributedCache _cache;

var cachedData = await _cache.GetStringAsync("key");
if (cachedData == null)
{
    var data = await GetFromDatabase();
    await _cache.SetStringAsync("key", JsonSerializer.Serialize(data),
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
        });
}

When to Use IMemoryCache:

✅ Use IMemoryCache for:
- Frequently accessed, rarely changed data
- Single-server applications
- Sub-second response times needed
- Small to medium datasets (< 100MB)
- Configuration data, lookup tables

❌ Don't use for:
- Data that changes frequently
- Multi-server deployments (use Redis)
- User session data (use cookies/Redis)
- Very large datasets (> 1GB)
- Data that must persist across restarts

Related posts:

.NET Core: Use Minimal APIs for Lightweight HTTP Services

.NET Core — Thread Pool Starvation Looks Like CPU Idle

ASP.NET Core Requests Hang Randomly

Post Views: 7

Post navigation

Git: Find and Remove Sensitive Data from Entire Repository History
.NET Core: Use Minimal APIs to Create Lightweight Endpoints Without Controllers

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

June 2026
M T W T F S S
1234567
891011121314
15161718192021
22232425262728
2930  
« May    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (953)
  • How to add default value for Entity Framework migrations for DateTime and Bool (882)
  • Get the First and Last Word from a String or Sentence in SQL (838)
  • How to select distinct rows in a datatable in C# (808)
  • How to make theater mode the default for Youtube (806)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (580)
  • Add Constraint to SQL Table to ensure email contains @ (580)
  • Average of all values in a column that are not zero in SQL (540)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (506)
  • Find numbers with more than two decimal places in SQL (455)

Recent Posts

  • C#: Use String Interpolation Instead of Concatenation
  • C#: Use Tuples to Return Multiple Values from Methods
  • SQL: Use ISNULL and NULLIF for Smart NULL Handling
  • .NET Core: Use Data Annotations for Model Validation
  • Git: Use Git Clean to Remove Untracked Files
  • Ajax: Add Custom Headers to Fetch Requests
  • JavaScript: Use console.table to Display Arrays as Tables
  • HTML: Use Spellcheck Attribute to Enable Browser Spell Check
  • CSS: Use user-select to Prevent Text Selection
  • Windows 11: Use Snipping Tool for Instant Screenshots

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (953)
  • How to add default value for Entity Framework migrations for DateTime and Bool (882)
  • Get the First and Last Word from a String or Sentence in SQL (838)
  • How to select distinct rows in a datatable in C# (808)
  • How to make theater mode the default for Youtube (806)

Recent Posts

  • C#: Use String Interpolation Instead of Concatenation
  • C#: Use Tuples to Return Multiple Values from Methods
  • SQL: Use ISNULL and NULLIF for Smart NULL Handling
  • .NET Core: Use Data Annotations for Model Validation
  • Git: Use Git Clean to Remove Untracked Files

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com