Description
Ideally for preview 6...
Background and Motivation
We wish to implement an official SE.Redis output cache implementation.
The existing IOutputCacheStore
API is byte[]
-focused, which is fine for the in-process implementation which constantly returns the same byte[]
, but is hugely suboptimal for out-of-process implementations, as we would have to return a right-sized byte[]
every call (in either direction), causing GC load. Additionally, since output-cache is an entire page load plus headers, this can easily be large enough to spill into LOH.
Context: #48450
IMPORTANT: this API was approved for RC6, but an unforeseen problem arose (also #50402) where-by this change added a framework reference into a previously non-framework-reference OOB package; this is undesirable, so a secondary change is proposed to resolve this. The obsolete parts of this proposal have been struck, and the new changes are in italics.
For this, we propose three things:
- a new
SetAsync
method similar to the existing, but takingReadOnlySequence<byte>
instead ofbyte[]
; this API to be implemented in the existingIOutputCacheStore
interface as a DIM, to avoid back-compat problems - a new interface
IOutputCacheBufferWriterStore
which extendsIOutputCacheStore
(same assembly/namespace), allowing implementations to optionally use a newGetAsync
API that takes anIBufferWriter<byte>
- thus allowing implementations to pass data to the consumer while site-stepping the topic of "data lifetime" : the receiver can pass a recycling-enabled buffer-writer, and the cache implementation does not need to know about those details - it just asks for space to write data, and writes - a new API to register the SE.Redis output cache implementation,
in the existing Microsoft.Extensions.Caching.StackExchangeRedis OOB packagein a new Microsoft.AspNetCore.OutputCaching.StackExchangeRedis OOB package
Proposed API
namespace Microsoft.AspNetCore.OutputCaching;
public interface IOutputCacheStore
{
// pre-existing
ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken);
+ ValueTask SetAsync(string key, ReadOnlySequence<byte> value, ReadOnlyMemory<string> tags, TimeSpan validFor, CancellationToken cancellationToken)
+ {
+ // compatibility implementation using the original API
+ return SetAsync(key, value.ToArray(), tags.IsEmpty ? null : tags.ToArray(), validFor, cancellationToken);
+ }
}
+ public interface IOutputCacheBufferWriterStore : IOutputCacheStore
+ {
+ ValueTask<bool> GetAsync(string key, IBufferWriter<byte> destination, CancellationToken cancellationToken);
+ }
This second interface is optional / separate because not all types will wish to opt in; in particular, in the case of the existing in-proc implementation, it will be preferable to just keep returning the existing byte[]
; as such, the output cache middleware will test for this API and use the most appropriate.
Additionally, to implement the SE.Redis cache, we propose the addition of
+ public static IServiceCollection AddStackExchangeRedisOutputCache(
+ this IServiceCollection services, Action<RedisCacheOptions> setupAction)
package: Microsoft.AspNetCore.OutputCaching.StackExchangeRedis
(new)
type Microsoft.AspNetCore.OutputCaching.StackExchangeRedis
(new)
+ public static IServiceCollection AddStackExchangeRedisOutputCache(
+ this IServiceCollection services, Action<RedisOutputCacheOptions> setupAction)
where RedisOutputCacheOptions
is a new clone of the existing RedisCacheOptions
, but in the new package/namespace (otherwise: 1-1 feature parity with RedisCacheOptions
):
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.Configuration.get -> string?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.Configuration.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConfigurationOptions.get -> StackExchange.Redis.ConfigurationOptions?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConfigurationOptions.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConnectionMultiplexerFactory.get -> System.Func<System.Threading.Tasks.Task<StackExchange.Redis.IConnectionMultiplexer!>!>?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ConnectionMultiplexerFactory.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.InstanceName.get -> string?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.InstanceName.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ProfilingSession.get -> System.Func<StackExchange.Redis.Profiling.ProfilingSession!>?
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.ProfilingSession.set -> void
+ Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.RedisOutputCacheOptions.RedisOutputCacheOptions() -> void
which is the new API to register SE.Redis into output cache
Alternatives considered
- eating the GC overhead, zero API change - hugely undesirable in this context
- have a new
GetAsync
API that returns some kind of "data with lifetime" - however, there's no good metaphor for this, and it pushes the buffer management into each individual buffer implementation, rather than having one buffer implementation and having the cache implementations just worry about cache