Introduction
Redis is an in-memory data structure store, used as a database, cache, and message broker. Additionally, it supports various data structures such as strings, hashes, lists, sets, sorted sets, etc. Consequently, this guide will walk you through implementing Redis cache in a .NET Core API step by step.
Prerequisites
Before we start, make sure you have the following installed:
- .NET Core SDK
- Redis Server
Step 1: Setting Up Your .NET Core API
- Create a new .NET Core Web API project:
dotnet new webapi -n RedisCacheExample
cd RedisCacheExample
- Add the necessary NuGet packages:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis dotnet add package StackExchange.Redis
Step 2: Configure Redis in .NET Core
Edit the
appsettings.jsonfile to include Redis configuration:{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Redis": { "ConnectionString": "localhost:6379" } }
Modify the Startup.cs file to add Redis caching services:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Add Redis caching
var redisConfiguration = Configuration.GetSection("Redis")["ConnectionString"];
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConfiguration;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Step 3: Implementing Redis Cache
Create a Cache Service:
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Text.Json;
using System.Threading.Tasks;
public class CacheService : ICacheService
{
private readonly IDistributedCache _cache;
public CacheService(IDistributedCache cache)
{
_cache = cache;
}
public async Task SetCacheAsync(string key, T value, TimeSpan expirationTime)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expirationTime
};
var serializedValue = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serializedValue, options);
}
public async Task GetCacheAsync(string key)
{
var serializedValue = await _cache.GetStringAsync(key);
if (serializedValue == null)
return default;
return JsonSerializer.Deserialize(serializedValue);
}
}
public interface ICacheService
{
Task SetCacheAsync(string key, T value, TimeSpan expirationTime);
Task GetCacheAsync(string key);
}
Register the Cache Service in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var redisConfiguration = Configuration.GetSection("Redis")["ConnectionString"];
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConfiguration;
});
// Register CacheService
services.AddTransient<ICacheService, CacheService>();
}
Step 4: Using Redis Cache in Controllers
Create a Sample Controller:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
private readonly ICacheService _cacheService;
private readonly string cacheKey = "sampleKey";
public SampleController(ICacheService cacheService)
{
_cacheService = cacheService;
}
[HttpGet]
public async Task Get()
{
var cachedData = await _cacheService.GetCacheAsync(cacheKey);
if (cachedData == null)
{
// Simulate data retrieval from a database or external service
var data = "This is some sample data";
await _cacheService.SetCacheAsync(cacheKey, data, TimeSpan.FromMinutes(1));
return Ok(new { Source = "Database", Data = data });
}
return Ok(new { Source = "Cache", Data = cachedData });
}
}
Step 5: Running the Application
Ensure Redis Server is running:
>redis-server
Run your .NET Core application:
>dotnet run
Test the API:
- First, open your browser or Postman and navigate to http://localhost:5000/api/sample.
- The first request should return data from the “database” (simulated).
- Afterwards, subsequent requests within one minute should return data from the cache.
You’ve now successfully set up a .NET Core API with Redis caching. Consequently, this implementation ensures that frequently accessed data can be retrieved quickly from the cache, thereby improving the performance and scalability of your application.”Advanced Configuration and Considerations
Now that you have a basic understanding of implementing Redis cache in a .NET Core API, let’s next delve into some advanced configurations and best practices to ultimately ensure your application is robust and efficient.
Handling Cache Expiration and Eviction
Redis provides multiple policies to manage cache expiration and eviction:
- Expiration Policies: Set absolute or sliding expiration for cached items.
- Absolute Expiration: The cache entry expires after a fixed duration from its creation time
- Sliding Expiration: Each access resets the cache entry’s expiration timer
- Eviction Policies: Redis supports different eviction policies to manage memory usage:
- volatile-lru: Removes least recently used keys that have expiration set
- allkeys-lru: Evicts least recently used keys from all stored keys
- volatile-ttl: Eliminates keys with the shortest remaining time-to-live
To configure however, you can adjust the Redis server configuration or set policies programmatically within your .NET Core application.
Configuring Sliding Expiration
Modify the CacheService to support sliding expiration:
public async Task SetCacheAsync(string key, T value, TimeSpan absoluteExpiration, TimeSpan slidingExpiration)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = absoluteExpiration,
SlidingExpiration = slidingExpiration
};
var serializedValue = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serializedValue, options);
}
Implementing Distributed Caching
In a distributed system whenever, it is crucial to have a shared cache accessible by all instances of your application. Redis is inherently designed to support distributed caching.
- Connecting to a Remote Redis Server: Update your
appsettings.jsonmoreover, the remote Redis server connection string.
"Redis": {
"ConnectionString": "your-remote-redis-server:6379"
}
- Scaling the API: Ensure your application instances are stateless and can leverage the shared Redis cache. usually Use a load balancer to distribute requests across multiple instances.
Implementing Cache Invalidation
Cache invalidation is essential to maintain data consistency. Here are some strategies:
- Time-Based Invalidation: Use cache expiration policies to automatically invalidate outdated data.
- Event-Based Invalidation: Invalidate cache entries based on specific events or triggers in your application.
For example, invalidate the cache when data is updated:
[HttpPost]
public async Task UpdateData([FromBody] string newData)
{
// Simulate updating data in the database
var updatedData = newData;
// Invalidate the cache
await _cacheService.SetCacheAsync(cacheKey, updatedData, TimeSpan.FromMinutes(1));
return Ok(new { Data = updatedData });
}
Monitoring and Troubleshooting
- Monitoring Tools: Use monitoring tools like RedisInsight, Datadog, or New Relic to monitor Redis performance and health.
- Logging: Implement logging to track cache hits, misses, and exceptions for troubleshooting.
Security Considerations
- Authentication: Configure Redis with a password and use SSL/TLS to encrypt data in transit.
- Access Control: Restrict access to the Redis server using firewall rules or security groups.
Sample Project Structure
Here’s a quick overview of the project structure:
RedisCacheExample/ ├── Controllers/ │ └── SampleController.cs ├── Services/ │ └── CacheService.cs ├── appsettings.json ├── Program.cs ├── Startup.cs
Full Example Code
appsettings.json{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Redis": {
"ConnectionString": "localhost:6379"
}
}
Startup.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var redisConfiguration = Configuration.GetSection("Redis")["ConnectionString"];
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConfiguration;
});
services.AddTransient<ICacheService, CacheService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
CacheService.cs
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Text.Json;
using System.Threading.Tasks;
public class CacheService : ICacheService
{
private readonly IDistributedCache _cache;
public CacheService(IDistributedCache cache)
{
_cache = cache;
}
public async Task SetCacheAsync(string key, T value, TimeSpan expirationTime)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expirationTime
};
var serializedValue = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serializedValue, options);
}
public async Task GetCacheAsync(string key)
{
var serializedValue = await _cache.GetStringAsync(key);
if (serializedValue == null)
return default;
return JsonSerializer.Deserialize(serializedValue);
}
}
public interface ICacheService
{
Task SetCacheAsync(string key, T value, TimeSpan expirationTime);
Task GetCacheAsync(string key);
}
SampleController.cs
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
private readonly ICacheService _cacheService;
private readonly string cacheKey = "sampleKey";
public SampleController(ICacheService cacheService)
{
_cacheService = cacheService;
}
[HttpGet]
public async Task Get()
{
var cachedData = await _cacheService.GetCacheAsync(cacheKey);
if (cachedData == null)
{
var data = "This is some sample data";
await _cacheService.SetCacheAsync(cacheKey, data, TimeSpan.FromMinutes(1));
return Ok(new { Source = "Database", Data = data });
}
return Ok(new { Source = "Cache", Data = cachedData });
}
[HttpPost]
public async Task UpdateData([FromBody] string newData)
{
var updatedData = newData;
await _cacheService.SetCacheAsync(cacheKey, updatedData, TimeSpan.FromMinutes(1));
return Ok(new { Data = updatedData });
}
}
Implementing Redis cache in a .NET Core API can significantly enhance performance by reducing the load on the database and providing quick access to frequently accessed data. By following the steps outlined in this guide, you can set up and manage Redis caching effectively. Remember to monitor your cache performance and handle security configurations appropriately to maintain a robust and efficient system.
Additional Considerations and Features
To further enhance your implementation of Redis cache in a .NET Core API, consider the following advanced features and best practices:
Using Redis for Session Storage
Redis can be used to store session data, which is particularly useful for distributed applications where multiple instances of the app need to share session state.
Install the necessary package:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Configure session storage in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var redisConfiguration = Configuration.GetSection("Redis")["ConnectionString"];
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConfiguration;
});
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddTransient<ICacheService, CacheService>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseSession(); // Enable session middleware
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Using sessions in a controller:
[ApiController]
[Route("api/[controller]")]
public class SessionController : ControllerBase
{
[HttpGet("set")]
public IActionResult SetSession()
{
HttpContext.Session.SetString("SessionKey", "This is a session value");
return Ok("Session value set.");
}
[HttpGet("get")]
public IActionResult GetSession()
{
var sessionValue = HttpContext.Session.GetString("SessionKey");
if (sessionValue == null)
{
return NotFound("Session value not found.");
}
return Ok($"Session value: {sessionValue}");
}
}
Handling Cache Dependencies
Sometimes, cached data may depend on other data, and when the dependent data changes, the cache should be invalidated. Implementing such logic ensures that your cache remains consistent with the underlying data.
Invalidate related cache entries when a dependent data change occurs:
[HttpPost("update")]
public async Task UpdateData([FromBody] string newData)
{
var updatedData = newData;
await _cacheService.SetCacheAsync(cacheKey, updatedData, TimeSpan.FromMinutes(1));
// Invalidate related cache entries
await _cacheService.RemoveCacheAsync(relatedCacheKey);
return Ok(new { Data = updatedData });
}
Implement the RemoveCacheAsync method in CacheService:
public async Task RemoveCacheAsync(string key)
{
await _cache.RemoveAsync(key);
}
Using Redis Pub/Sub for Real-time Updates
Redis Pub/Sub can be used to implement real-time notifications or updates. For instance, you can notify multiple instances of an application when a cache entry is updated or invalidated.
Set up a subscriber service:
public class RedisSubscriberService : BackgroundService
{
private readonly IConnectionMultiplexer _redis;
private readonly ICacheService _cacheService;
public RedisSubscriberService(IConnectionMultiplexer redis, ICacheService cacheService)
{
_redis = redis;
_cacheService = cacheService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = _redis.GetSubscriber();
await subscriber.SubscribeAsync("cache-updates", async (channel, message) =>
{
await _cacheService.RemoveCacheAsync(message);
});
}
}
Register the subscriber service in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var redisConfiguration = Configuration.GetSection("Redis")["ConnectionString"];
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConfiguration;
});
services.AddSingleton(ConnectionMultiplexer.Connect(redisConfiguration));
services.AddHostedService();
services.AddTransient<ICacheService, CacheService>();
}
Publish messages when cache entries are updated:
public async Task SetCacheAsync(string key, T value, TimeSpan expirationTime)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expirationTime
};
var serializedValue = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serializedValue, options);
var subscriber = _redis.GetSubscriber();
await subscriber.PublishAsync("cache-updates", key);
}
This comprehensive guide has covered the implementation of Redis cache in a .NET Core API, including advanced configurations, distributed caching, session storage, cache invalidation strategies, and real-time updates using Redis Pub/Sub. These techniques and best practices will help you create a scalable, efficient, and robust caching layer for your application.
By leveraging Redis, you can significantly enhance the performance and responsiveness of your .NET Core applications, making them more capable of handling high loads and providing a better user experience.