Implementing Distributed Caching with Redis in Azure Functions

mbrennan376

Michael Brennan

Posted on November 1, 2024

Implementing Distributed Caching with Redis in Azure Functions

In modern applications, distributed caching is an essential tool for improving performance and scalability. One popular option for distributed caching is Redis, an in-memory key-value store that can handle large amounts of data with low latency.

In this guide, we'll implement distributed caching using Redis in an Azure Function App, following clean architecture principles. We'll also follow best practices by injecting services using interfaces, which makes the code more flexible and easier to test.

Project Structure

The project follows the clean architecture pattern, splitting the code into distinct layers:

  • FunctionApp: Contains the Azure Function entry point.
  • Application: Handles business logic and service classes.
  • Domain: Defines the core entities.
  • Infrastructure: Implements external dependencies like Redis caching and repositories.

Solution Folder Structure

AzureFunctionRedisCleanArchitecture/
├── AzureFunctionRedisCleanArchitecture.sln
├── FunctionApp/
│   ├── FunctionApp.csproj
│   ├── Startup.cs
│   └── Functions/
│       └── ProductFunction.cs
├── Application/
│   ├── Application.csproj
│   ├── Extensions/
│   │   └── ApplicationServicesExtensions.cs
│   ├── Interfaces/
│   │   └── IProductService.cs
│   ├── Services/
│       └── ProductService.cs
├── Domain/
│   ├── Domain.csproj
│   └── Entities/
│       └── Product.cs
└── Infrastructure/
    ├── Infrastructure.csproj
    ├── Extensions/
    │   └── InfrastructureServicesExtensions.cs
    └── Repositories/
        └── CachedProductRepository.cs
Enter fullscreen mode Exit fullscreen mode

1. Azure Function App

The FunctionApp is the entry point for our Azure Function. It will handle HTTP requests and use the services defined in the Application layer. Here, we inject IProductService instead of the concrete ProductService, following the Dependency Inversion Principle.

FunctionApp/Startup.cs

This file configures the dependency injection for the Function App, pulling in both the Application and Infrastructure services.

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Application.Extensions;
using Infrastructure.Extensions;

[assembly: FunctionsStartup(typeof(FunctionApp.Startup))]

namespace FunctionApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            string redisConnectionString = Environment.GetEnvironmentVariable("RedisConnectionString");

            builder.Services
                .AddApplicationServices()      // Add business services
                .AddInfrastructureServices(redisConnectionString);  // Add Redis caching and repositories
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

FunctionApp/Functions/ProductFunction.cs

This function handles HTTP requests to fetch a product. It uses the IProductService interface from the Application layer, which interacts with the repository layer to fetch the product from the cache or database.

using Application.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace FunctionApp.Functions
{
    public class ProductFunction
    {
        private readonly IProductService _productService;

        public ProductFunction(IProductService productService)
        {
            _productService = productService;
        }

        [FunctionName("GetProduct")]
        public async Task<IActionResult> GetProduct(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "product/{id:int}")] HttpRequest req, int id)
        {
            var product = await _productService.GetProductByIdAsync(id);
            return new OkObjectResult(product);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Application Layer

The Application layer contains business logic. We have added the IProductService interface to abstract the business logic provided by ProductService. This interface allows us to inject the service easily and makes testing easier.

Application/Interfaces/IProductService.cs

using Domain.Entities;
using System.Threading.Tasks;

namespace Application.Interfaces
{
    public interface IProductService
    {
        Task<Product> GetProductByIdAsync(int id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Application/Services/ProductService.cs

The ProductService implements the IProductService interface. It uses the repository to fetch data from Redis or the simulated database.

using Application.Interfaces;
using System.Threading.Tasks;
using Domain.Entities;

namespace Application.Services
{
    public class ProductService : IProductService
    {
        private readonly IProductRepository _productRepository;

        public ProductService(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        public async Task<Product> GetProductByIdAsync(int id)
        {
            return await _productRepository.GetProductByIdAsync(id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Cache Invalidation on Update

When updating the product, we need to ensure the cache is invalidated (or refreshed) to avoid stale data. The CachedProductRepository class has an UpdateProductAsync method that removes the old product from the cache after updating it in the database.

Infrastructure/Repositories/CachedProductRepository.cs

This repository interacts with Redis to cache and fetch the product, and it invalidates the cache after the product is updated.

using Application.Interfaces;
using Domain.Entities;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace Infrastructure.Repositories
{
    public class CachedProductRepository : IProductRepository
    {
        private readonly IDistributedCache _cache;

        public CachedProductRepository(IDistributedCache cache)
        {
            _cache = cache;
        }

        public async Task<Product> GetProductByIdAsync(int id)
        {
            var cacheKey = $"Product_{id}";
            var cachedProduct = await _cache.GetStringAsync(cacheKey);

            if (!string.IsNullOrEmpty(cachedProduct))
            {
                return JsonConvert.DeserializeObject<Product>(cachedProduct);
            }

            // Simulate fetching from database (placeholder for actual DB logic)
            var product = new Product { Id = id, Name = $"Product {id}", Price = 100 + id };

            var productJson = JsonConvert.SerializeObject(product);
            await _cache.SetStringAsync(cacheKey, productJson, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            });

            return product;
        }

        public async Task UpdateProductAsync(Product product)
        {
            // Simulate updating the product in the database

            // Invalidate cache for the updated product
            var cacheKey = $"Product_{product.Id}";
            await _cache.RemoveAsync(cacheKey);

            // Optionally, re-add the updated product to the cache
            var productJson = JsonConvert.SerializeObject(product);
            await _cache.SetStringAsync(cacheKey, productJson, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Dependency Injection for Infrastructure

We use Redis for caching in this repository, and it is registered in the DI container through an extension method.

In our implementation, we use the IDistributedCache interface provided by .NET. It abstracts the distributed caching system, allowing us to use different caching providers like Redis. You can read more about IDistributedCache in the official Microsoft documentation.

Infrastructure/Extensions/InfrastructureServicesExtensions.cs

using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
using Application.Interfaces;
using Infrastructure.Repositories;
using Microsoft.Extensions.Caching.StackExchangeRedis;

namespace Infrastructure.Extensions
{
    public static class InfrastructureServicesExtensions
    {
        public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, string redisConnectionString)
        {
            // Set up Redis distributed caching
            services.AddStackExchangeRedisCache(options =>
            {
                options.Configuration = redisConnectionString;
            });

            // Register Redis connection
            services.AddSingleton<IConnectionMultiplexer>(sp =>
                ConnectionMultiplexer.Connect(redisConnectionString));

            // Register repository with caching
            services.AddScoped<IProductRepository, CachedProductRepository>();

            return services;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this example, we’ve used Redis as a distributed caching solution in an Azure Function App. The ProductFunction fetches products using a service, which interacts with the caching layer. When a product is updated, the cache is invalidated to ensure that the next request fetches the fresh data.

This setup improves performance by reducing database load while ensuring cache consistency after updates.

💖 💪 🙅 🚩
mbrennan376
Michael Brennan

Posted on November 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related