Implementing Distributed Caching with Redis in Azure Functions
Michael Brennan
Posted on November 1, 2024
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
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
}
}
}
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);
}
}
}
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);
}
}
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);
}
}
}
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)
});
}
}
}
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;
}
}
}
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.
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
November 27, 2024