Skip to main content

Caching

Several ways of caching data are available in an asp.net project. Some will be described here.

Output caching

Output caching, available in asp.net core, can be used to cache GraphQL requests/responses.

Register required services (builder.Services.AddXyz()) and set up the request pipeline (app.UseXyz()) appropriately.

Typical asp.net Program.cs:

var builder = WebApplication.CreateBuilder(args);

[...]

builder.Services.AddOutputCache();

[...]

var app = builder.Build();

[...]

app.UseOutputCache();

[...]

app.MapGraphQL<MyDbContext>(
configureEndpoint: endpointConventionBuilder =>
endpointConventionBuilder.CacheOutput(outputCachePolicyBuilder =>
{
// Support HTTP POST requests, not supported by default
outputCachePolicyBuilder.AddPolicy<GraphQLPolicy>();
outputCachePolicyBuilder.VaryByValue(httpContext =>
{
httpContext.Request.EnableBuffering();
var initialBodyStreamPosition = httpContext.Request.Body.Position;

httpContext.Request.Body.Position = 0;

using var bodyReader = new StreamReader(httpContext.Request.Body, leaveOpen: true);
var bodyContent = bodyReader.ReadToEndAsync()
.Result
.Replace(" ", "");

httpContext.Request.Body.Position = initialBodyStreamPosition;

return new KeyValuePair<string, string>("requestBody", bodyContent);
});
outputCachePolicyBuilder.Expire(TimeSpan.FromSeconds(MedicalDataApiConstants.OutputCache.DefaultExpireSeconds));
}
));
  • The code contained in the VaryByValue function will be called by the asp.net pipeline, from the OutputCacheMiddleware for every GraphQL request. If the same key, as the one returned by the function, is being found in the cache, its cached value will be returned. If it is not found, EntityGraphQL will execute the DB call and the returned result will be added to the cache.
  • Adapt the VaryByValue function to your needs. E.g. if the GraphQL query depends on the accept language, it can be added as part of the key.
  • With builder.Services.AddOutputCache(), in memory cache will be used. It can also be configured to use other backends e.g. Redis.

We have to define and register our own IOutputCachePolicy, because the DefaultPolicy only supports caching for HTTP GET endpoints, whereas GraphQL uses HTTP POST. Note that all implementations of IOutputCachePolicy are defined as internal sealed and thus, can't be extended.
The below GraphQLPolicy is a copy/paste of DefaultPolicy, where only HTTP POST requests are supported, instead of GET.

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace MyApi.OutputCachePolicies;

internal sealed class GraphQLPolicy : IOutputCachePolicy
{
/// <inheritdoc />
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
var attemptOutputCaching = AttemptOutputCaching(context);
context.EnableOutputCaching = true;
context.AllowCacheLookup = attemptOutputCaching;
context.AllowCacheStorage = attemptOutputCaching;
context.AllowLocking = true;

// Vary by any query by default
context.CacheVaryByRules.QueryKeys = "*";

return ValueTask.CompletedTask;
}

/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}

/// <inheritdoc />
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
var response = context.HttpContext.Response;

// Verify existence of cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
{
context.AllowCacheStorage = false;
return ValueTask.CompletedTask;
}

// Check response code
if (response.StatusCode != StatusCodes.Status200OK)
{
context.AllowCacheStorage = false;
return ValueTask.CompletedTask;
}

return ValueTask.CompletedTask;
}

private static bool AttemptOutputCaching(OutputCacheContext context)
{
// Check if the current request fulfills the requirements to be cached

var request = context.HttpContext.Request;

// Verify the method
// Only allow POST requests to be cached (only change from DefaultPolicy)
if (!HttpMethods.IsPost(request.Method))
{
return false;
}

// Verify existence of authorization headers
if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true)
{
return false;
}

return true;
}
}

EFCore Second Leve Cache Interceptor

EFCoreSecondLevelCacheInterceptor is a library that caches the result of EF commands and automatically invalidates cached data by watching what tables are updated via EF. This makes it a really easy way to speed up the performance of EntityGraphQL without the overhead of manual invalidation. EQL still needs to parse queries and convert them to expression trees, so there's still more cpu overhead than output caching.

It supports numerous caching methods through its support of EasyCaching.Core (InMemory, Redis, Sql, Disk + more).

For full setup instructions see their documentation on their github page (https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor) however it can be as simple as enabling efsecondlevelcache, adding a caching provider and adding the interceptor to EFCore.

  services.AddEFSecondLevelCache(options => {
options.UseMemoryCacheProvider();
});

services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) => {
optionsBuilder
.UseSqlServer(...)
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()));
});