Azure Functions v3 Response Caching

Azure Functions (v3): Response Caching

This content may be outdated. Please, read this page keeping its age in your mind.

This article describes how to add response caching to an Azure Functions v3 App. Response Caching can reduce the number of requests to your Functions significantly which helps you save costs and boosts performance.

In any asp.net core web app we could just add the response cache middleware, use the ResponseCache attribute and we are done. Unfortunately, the current functions runtime does not support running any middleware out of the box, so I decided to mimic the basic behavior of the ResponseCache-Attribute using my own attribute.

Be aware that my implementation has the following consequences:

  • You cannot use model binding (which is very limited anyway)
  • VaryBy is not supported
  • Cache Profiles are not supported (but could be implemented)
  • The attribute uses FunctionInvocationFilterAttribute which is currently in preview

Required NuGet Packages

<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />

Azure Functions Response Caching: The Code

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Net.Http.Headers;

namespace tebest.Func.Shared
{
    public class FunctionResponseCacheAttribute : FunctionInvocationFilterAttribute
    {
        private readonly int _duration;
        private readonly ResponseCacheLocation _cacheLocation;

        public FunctionResponseCacheAttribute(
            int duration, 
            ResponseCacheLocation cacheLocation)
        {
            _duration = duration;
            _cacheLocation = cacheLocation;
        }


        public override async Task OnExecutedAsync(
            FunctionExecutedContext executedContext, 
            CancellationToken cancellationToken)
        {
            if (!(executedContext.Arguments.First().Value is HttpRequest request))
                throw new ApplicationException(
                    "HttpRequest is null. ModelBinding is not supported, " +
                    "please use HttpRequest as input parameter and deserialize " +
                    "using helper functions.");
            var headers = request.HttpContext.Response.GetTypedHeaders();

            var cacheLocation = executedContext.FunctionResult?.Exception == null 
                ? _cacheLocation 
                : ResponseCacheLocation.None;

            switch (cacheLocation)
            {
                case ResponseCacheLocation.Any:
                    headers.CacheControl = new CacheControlHeaderValue()
                    {
                        MaxAge = TimeSpan.FromSeconds(_duration),
                        NoStore = false,
                        Public = true
                    };
                    break;
                case ResponseCacheLocation.Client:
                    headers.CacheControl = new CacheControlHeaderValue()
                    {
                        MaxAge = TimeSpan.FromSeconds(_duration),
                        NoStore = false,
                        Public = true
                    };
                    break;
                case ResponseCacheLocation.None:
                    headers.CacheControl = new CacheControlHeaderValue()
                    {
                        MaxAge = TimeSpan.Zero,
                        NoStore = true
                    };
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            await base.OnExecutedAsync(executedContext, cancellationToken);
        }
    }
}

Usage

[FunctionName(nameof(GetCars))]
[FunctionResponseCache(60 * 60, ResponseCacheLocation.Any)]
[ProducesResponseType(typeof(IEnumerable<CarListItemPresenter>), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetCars(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "v1/cars")]
	HttpRequest req,
	ILogger log)
{
	return await _carService.GetCarList(ps);
}

Notes

If the Function result is an exception, the code will add the necessary headers to not cache the result, despite what is set in the attribute. You should probably fine-tune this to check for appropriate HTTP response status codes, as you may not just throw exceptions in every scenario.

Recommended Reads

This post contains affiliate links. Clicking on those links helps me running my blog and does not add any additional costs! Thank you for your help!

1 Comment

Leave a Reply