Just like venturing into the wilderness requires the proper gear, developing REST APIs with ASP.NET Core also requires the right tools and equipment for your application to perform at its best.
At Trailhead, Redis caching is one such tool that’s always in our backpack.
What Is Redis?
Redis is an open-source, in-memory data store that can be used as a database, cache, and message broker. It’s blazing fast and can store and retrieve data quickly. By using Redis caching, you can significantly improve the performance and scalability of your ASP.NET Core application.
How Redis Speeds Up Your API
Redis can be used for special purposes such as session management, user authentication, push notifications, and event-driven programming. However, the primary purpose for using Redis caching in a .NET API is to improve the performance and speed of the application by taking load off your database. You see, when you cache frequently accessed data in Redis and access it there, you reduce the number of requests made to the database, thereby improving the overall performance of the application.
Let’s look at an example. Imagine a scenario where your application has 4 endpoints:
- GetCountries,
- GetProfilePicture,
- CreateOrder, and
- GetCustomerOrders.
Now imagine that the first two endpoints only retrieve data, that the data changes very infrequently, and that the endpoints are accessed hundreds or thousands of times per minute. The second two endpoints are used much less frequently, but they retrieve and create records that are much more critical to the mission and success of your application.
Depending on the number of users and the resources available to your database, you could easily overload the database with just the first two read-only endpoints, leaving the two more-critical endpoints very slow, or even failing.
Instead, you could cache your countries’ lookup data and profile pictures in Redis and only update them from the database once per hour, or when a pre-defined event occurs (such as a user updating their profile picture through the UI). This takes almost all the load for your first two endpoints off of your database, allowing it to focus on more mission-critical tasks.
Next, let’s look at some common scenarios when you might use Redis caching in your .NET API application: lookup data, user permissions, slow queries, and SignalR/websocket push notifications.
Example: Lookup Data
Many applications will have some lookup data in them, such as country codes, states/provinces, product categories, departments, time zones, or status codes. These values don’t change very often but may be retrieved frequently.
Typically, these lookup values are stored in a database, which works, but it keeps your database busy returning the same values over and over, instead of more complex queries or creating and updating records. Taking this load off your database can help your application scale much better by letting the database focus on what it does best.
public class CountryCodeCache
{
private static readonly Lazy<CountryCodeCache> lazy = new Lazy<CountryCodeCache>(() => new CountryCodeCache());
public static CountryCodeCache Instance => lazy.Value;
private readonly IDatabase redisDb;
private readonly string countryCodesKey = "CountryCodes";
private CountryCodeCache()
{
var redis = ConnectionMultiplexer.Connect("localhost:6379");
redisDb = redis.GetDatabase();
}
public List<string> CountryCodes
{
get
{
var countryCodes = redisDb.HashValues(countryCodesKey);
if (countryCodes.Length == 0)
{
// Load country codes from database
countryCodes = LoadCountryCodesFromDatabase();
// Set country codes in Redis cache
var hashEntries = countryCodes.Select((code, index) => new HashEntry(index, code)).ToArray();
redisDb.HashSet(countryCodesKey, hashEntries);
}
return countryCodes.Select(c => c.ToString()).ToList();
}
}
private string[] LoadCountryCodesFromDatabase()
{
// Load country codes from database query
var connectionString = ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString;
using (var connection = new SqlConnection(connectionString))
{
var query = "SELECT CountryCode FROM Countries ORDER BY CountryCode ASC";
var command = new SqlCommand(query, connection);
connection.Open();
var reader = command.ExecuteReader();
var countryCodes = new List<string>();
while (reader.Read())
{
countryCodes.Add(reader.GetString(0));
}
return countryCodes.ToArray();
}
}
}
Example: User Permissions
Another common use case for Redis caching in a .NET API is to store user permissions. In many applications, user permissions are checked on every request to ensure the user is authorized to access a specific resource. This can be a time-consuming and resource-intensive operation, especially if the permissions are stored in a database.
By caching the user permissions in Redis, the application can significantly improve performance by reducing the number of database queries required to authorize a user. Below is a simple example that shows you how you might implement this.
public class UserPermissionsCache
{
private static readonly Lazy<UserPermissionsCache> lazy = new Lazy<UserPermissionsCache>(() => new UserPermissionsCache());
public static UserPermissionsCache Instance => lazy.Value;
private readonly IDatabase redisDb;
private readonly string userPermissionsKeyPrefix = "UserPermissions:";
private UserPermissionsCache()
{
var redis = ConnectionMultiplexer.Connect("localhost:6379");
redisDb = redis.GetDatabase();
}
public bool HasPermission(int userId, string permission)
{
var key = $"{userPermissionsKeyPrefix}{userId}";
// Check if user permissions are in Redis cache
if (redisDb.HashExists(key, permission))
{
return true;
}
// Load user permissions from database
var userPermissions = LoadUserPermissionsFromDatabase(userId);
// Set user permissions in Redis cache
var hashEntry = new HashEntry(permission, true);
redisDb.HashSet(key, hashEntry);
return userPermissions.Contains(permission);
}
private List<string> LoadUserPermissionsFromDatabase(int userId)
{
// Load user permissions from database query
var connectionString = ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString;
using (var connection = new SqlConnection(connectionString))
{
var query = "SELECT Permission FROM UserPermissions WHERE UserId = @UserId";
var command = new SqlCommand(query, connection);
command.Parameters.AddWithValue("@UserId", userId);
connection.Open();
var reader = command.ExecuteReader();
var userPermissions = new List<string>();
while (reader.Read())
{
userPermissions.Add(reader.GetString(0));
}
return userPermissions;
}
}
}
Example: Slow EF Query
Entity Framework (EF) is a powerful object-relational mapping framework that simplifies data access in .NET applications. However, sometimes EF queries can be slow, especially when querying large datasets. In these cases, Redis caching can be used to improve the performance of the application.
By caching the results of slow EF queries in Redis, the application can avoid hitting the database every time the query is executed. Instead, the cached results can be retrieved from Redis, which is much faster than querying the database. To implement this, you can store the serialized results of the EF query in Redis using a unique key.
public class SlowQueryCache
{
private static readonly Lazy<SlowQueryCache> lazy = new Lazy<SlowQueryCache>(() => new SlowQueryCache());
public static SlowQueryCache Instance => lazy.Value;
private readonly IDatabase redisDb;
private readonly string slowQueryKey = "SlowQuery";
private SlowQueryCache()
{
var redis = ConnectionMultiplexer.Connect("localhost:6379");
redisDb = redis.GetDatabase();
}
public List<Widget> GetWidgets()
{
var widgets = redisDb.Get<List<Widget>>(slowQueryKey);
if (widgets == null)
{
// Load widgets from slow Entity Framework query
widgets = LoadWidgetsFromDatabase();
// Set widgets in Redis cache
redisDb.Set(slowQueryKey, widgets, TimeSpan.FromMinutes(5));
}
return widgets;
}
private List<Widget> LoadWidgetsFromDatabase()
{
using (var context = new MyDbContext())
{
return context.Widgets.Include(w => w.WidgetParts).ToList();
}
}
}
Example: SignalR Push Notifications
SignalR is a popular library for adding real-time web functionality to .NET applications. One common use case for SignalR is to push notifications to connected clients when new data is available. However, constantly querying the database to check for new data can be slow and inefficient.
By using Redis caching, you can store the status of the data in Redis and query it instead of the database. This can significantly improve the performance of the application by reducing the number of database queries required to push notifications to connected clients. To implement this, you can store the status of the data in Redis using a unique key and subscribe to changes using SignalR. When the status of the data changes, SignalR can push a notification to connected clients.
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using StackExchange.Redis;
public class NotificationHub : Hub
{
private readonly IDatabase _redisDb;
private readonly string _notificationKey = "Notifications";
public NotificationHub()
{
var redis = ConnectionMultiplexer.Connect("localhost:6379");
_redisDb = redis.GetDatabase();
}
public async Task Subscribe(string userId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
}
public async Task Unsubscribe(string userId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId);
}
public async Task SendNotification(string userId, string message)
{
// Save notification in Redis
var notification = new Notification
{
UserId = userId,
Message = message
};
var serializedNotification = JsonConvert.SerializeObject(notification);
_redisDb.ListRightPush(_notificationKey, serializedNotification);
// Send notification to user if they are currently connected
await Clients.Group(userId).SendAsync("ReceiveNotification", notification);
}
public override async Task OnConnectedAsync()
{
var userId = Context.User.Identity.Name;
await Subscribe(userId);
// Send any pending notifications to the newly connected user
var serializedNotifications = _redisDb.ListRange(_notificationKey);
foreach (var serializedNotification in serializedNotifications)
{
var notification = JsonConvert.DeserializeObject<Notification>(serializedNotification);
if (notification.UserId == userId)
{
await Clients.Caller.SendAsync("ReceiveNotification", notification);
}
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userId = Context.User.Identity.Name;
await Unsubscribe(userId);
await base.OnDisconnectedAsync(exception);
}
}
public class Notification
{
public string UserId { get; set; }
public string Message { get; set; }
}
In Summary
As you can see, Redis caching can make your application perform and scale better by taking a load off your database for frequently accessed data that changes infrequently. I’ve shown you some examples of how Trailhead often uses it. How else do you use Redis caching in your .NET APIs?