How to Use ASP.NET Core Output Caching

The built-in support for Output Caching (sometimes called Response Caching) in ASP.NET Core is one example of this capability. By caching the output and serving it straight from the cache for upcoming requests, output caching enables you to significantly increase the performance of your APIs. For often visited, cacheable responses, this can greatly lessen the strain on your database and server.

Enabling Output Caching

To utilize Output Caching with your ServiceStack Endpoints, you first need to add the Output Caching middleware to your ASP.NET Core request pipeline in the Configure method of your Program.cs:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
app.UseOutputCache();
// ...
app.UseServiceStack(new AppHost(), options => options.MapEndpoints());

Then in ConfigureServices you need to add the Output Caching services:

services.AddOutputCache();

This will mostly rely on your application and the dependencies you already use, as the sequence in which you add OutputCache to your request pipeline might be highly vulnerable to change. An illustration of its use in a Blazor application may be found below.

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();
// Add OutputCache after Antiforgery and before Auth related middleware
app.UseOutputCache();

// Required for OutputCache
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

// Add additional endpoints required by the Identity /Account Razor components.
app.MapAdditionalIdentityEndpoints();

app.UseServiceStack(new AppHost(), options => {
    options.MapEndpoints();
});

Configuring Caching Behavior

Now that the middleware is installed, you can register against the Route Handlers in the ServiceStack options to set up caching behaviors for your ServiceStack Endpoints.

app.UseServiceStack(new AppHost(), options => {
    options.MapEndpoints();
    options.RouteHandlerBuilders.Add((routeHandlerBuilder, operation, verb, route) =>
    {
        routeHandlerBuilder.CacheOutput(c =>
        {
            // Use Cache Profiles
            c.UseProfile("Default30");

            // Or configure caching per-request
            c.Expire(TimeSpan.FromSeconds(30));
            c.VaryByAll();
        });
    });
});

Additionally, you can change the cache based on particular attributes, such as:

builder.CacheOutput(c => c.VaryBy("userRole","region"));

Alternatively, for reusable caching techniques, utilize Cache Profiles:

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("Default30", p => p.Expire(TimeSpan.FromSeconds(30)));
});

Next, set up the specified profile on your endpoints:

builder.CacheOutput(c => c.UseProfile("Default30"));
 

Finer-grained Control

For more granular control, you can apply the [OutputCache] attribute directly on your Service class, and use the ServiceStack AppHost metadata in your RouteHandlerBuilderAdd method to detect and cache only the routes that are attributed with OutputCache.

app.UseServiceStack(new AppHost(), options => {
    options.MapEndpoints();
    options.RouteHandlerBuilders.Add((routeHandlerBuilder, operation, verb, route) =>
    {
        // Initialized appHost and allServiceTypes
        var appHost = HostContext.AppHost;
        var allServiceTypes = appHost.Metadata.ServiceTypes;

        // Find the service matching the RequestType of the operation
        var operationType = operation.RequestType;
        // Match with operation, verb and route
        appHost.Metadata.OperationsMap.TryGetValue(operationType, out var operationMap);
        var serviceType = operationMap?.ServiceType;
        if (serviceType == null)
            return;
        if (serviceType.HasAttributeOf<OutputCacheAttribute>())
        {
            // Handle duration from OutputCacheAttribute
            var outputCacheAttribute = serviceType.FirstAttribute<OutputCacheAttribute>();
            routeHandlerBuilder.CacheOutput(policyBuilder =>
            {
                policyBuilder.Cache().Expire(TimeSpan.FromSeconds(outputCacheAttribute.Duration));
            });
        }
    });
});
[OutputCache(Duration = 60)]
public class MyServices : Service
{
    public object Any(Hello request)
    {
        return new HelloResponse { Result = $"Hello, {request.Name}!" };
    }
}

By modifying the code above in the ServiceStack options, you can expand your use of the built-in OutputCache feature and get fine-grained control that is compatible with using the same attribute with your MVC Controllers.

ServiceStack Redis Distributed Cache

The above examples so far have been using a cache store that comes with the OutputCache package. This is just an in memory store, so isn’t suitable for a distributed application. Thankfully, you can override the IOutputCacheStore interface in your IoC to change out the implementation that uses a centralized system like a Redis server.

public class RedisOutputCacheStore(IRedisClientsManager redisManager) : IOutputCacheStore
{
    public async ValueTask<byte[]?> GetAsync(string key, CancellationToken cancellationToken)
    { 
        await using var redis = await redisManager.GetClientAsync(token: cancellationToken);
        var value = await redis.GetAsync<byte[]>(key, cancellationToken);
        return value;
    }

    public async ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken)
    {
        await using var redis = await redisManager.GetClientAsync(token: cancellationToken);

        // First persist in normal cache hashset
        await redis.SetAsync(key, value, validFor, cancellationToken);

        if (tags == null)
            return;
        foreach (var tag in tags)
        {
            await redis.AddItemToSetAsync($"tag:{tag}", key, cancellationToken);
        }
    }

    public async ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken)
    {
        await using var redis = await redisManager.GetClientAsync(token: cancellationToken);

        var keys = await redis.GetAllItemsFromListAsync($"tag:{tag}", cancellationToken);

        foreach (var key in keys)
        {
            await redis.RemoveEntryAsync(key);
            await redis.RemoveItemFromSetAsync($"tag:{tag}", key, cancellationToken);
        }
    }
}

The ServiceStack.Redis client is used in the aforementioned straightforward implementation of the IOutputCacheStore to manage a centralized distributed cache. We can register our IoC dependencies in a Configure.OutputCache.cs file by using the aforementioned class.

[assembly: HostingStartup(typeof(BlazorOutputCaching.ConfigureOutputCache))]

namespace BlazorOutputCaching;

public class ConfigureOutputCache : IHostingStartup
{
    public void Configure(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.AddSingleton<IRedisClientsManager>(c =>
                new BasicRedisClientManager("localhost:6379"));
            services.AddSingleton<IOutputCacheStore, RedisOutputCacheStore>();
        });
    }
}

For our RedisOutputCacheStore, we first register the Redis client manager before launching the store.

Summary

ASP.NET Core Output Caching is a powerful tool for improving the performance of your ServiceStack endpoints. With ServiceStack 8.1’s tight integration with ASP.NET Core Endpoint Routing, utilizing this feature is now straightforward.

As always, caching is a balancing act. Apply it judiciously to frequently accessed, cacheable data. And be sure to implement appropriate invalidation strategies to keep your application’s data fresh.

By leveraging Output Caching effectively, you can dramatically improve the scalability and responsiveness of your ServiceStack powered applications. Try it out in your ServiceStack 8.1+ projects and let us know how it goes!

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *