Slow .NET API? Redis Caching May Be the Answer

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:

  1. GetCountries,
  2. GetProfilePicture,
  3. CreateOrder, and
  4. 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.

Critical endpoints slowed down by frequently accessed endpoints

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.

Frequently accessed data moved to Redis to take load of the database

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?

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.