Skip to content

Commit

Permalink
per feedback on #53255, try non-generic primary API
Browse files Browse the repository at this point in the history
  • Loading branch information
mgravell committed Feb 1, 2024
1 parent de07db3 commit 44e2984
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static IServiceCollection AddTypedDistributedCache(this IServiceCollectio
services.AddDistributedMemoryCache(); // we need a backend; use in-proc by default
services.TryAddSingleton(typeof(ICacheSerializer<string>), typeof(StringSerializer));
services.TryAddSingleton(typeof(ICacheSerializer<>), typeof(DefaultJsonSerializer<>));
services.AddSingleton(typeof(IDistributedCache<>), typeof(DistributedCache<>));
services.AddSingleton(typeof(IAdvancedDistributedCache), typeof(DistributedCache));
return services;
}
}
Expand Down
59 changes: 38 additions & 21 deletions src/Caching/Caching/src/DistributedCacheT.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Caching.Distributed;
internal sealed class DistributedCache<T> : IDistributedCache<T>
internal sealed class DistributedCache : IAdvancedDistributedCache
{
private readonly ICacheSerializer<T> _serializer;
private readonly IServiceProvider _services;
private readonly IDistributedCache _backend;
private readonly IBufferDistributedCache? _bufferBackend;

public DistributedCache(IOptions<TypedDistributedCacheOptions> options, ICacheSerializer<T> serializer, IDistributedCache backend)
public DistributedCache(IOptions<TypedDistributedCacheOptions> options, IServiceProvider services, IDistributedCache backend)
{
_serializer = serializer;
_services = services;
_backend = backend;
_bufferBackend = backend as IBufferDistributedCache; // do the type test once only
_ = options;
}

// for the simple usage scenario (no TState), pack the original callback as the "state", and use a wrapper function that just unrolls and invokes from the state
static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> _wrapped = static (callback, ct) => callback(ct);
public ValueTask<T> GetAsync(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
=> GetAsync(key, callback, _wrapped, options, cancellationToken);
static class WrappedCallbackCache<T>
{
// for the simple usage scenario (no TState), pack the original callback as the "state", and use a wrapper function that just unrolls and invokes from the state
public static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> Instance = static (callback, ct) => callback(ct);

}
public ValueTask<T> GetAsync<T>(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
=> GetAsync(key, callback, WrappedCallbackCache<T>.Instance, options, cancellationToken);

public ValueTask<T> GetAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
public ValueTask<T> GetAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
ArgumentNullException.ThrowIfNull(callback);

return _bufferBackend is not null
? GetBufferedBackendAsync(key, state, callback, options, cancellationToken)
: GetLegacyBackendAsync(key, state, callback, options, cancellationToken);

}

ValueTask IDistributedCache<T>.RefreshAsync(string key, CancellationToken cancellationToken) => new(_backend.RefreshAsync(key, cancellationToken));
ValueTask IAdvancedDistributedCache.RefreshAsync(string key, CancellationToken cancellationToken) => new(_backend.RefreshAsync(key, cancellationToken));

private ValueTask<T> GetBufferedBackendAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
private ValueTask<T> GetBufferedBackendAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
{
var buffer = new RecyclableArrayBufferWriter<byte>();
var pendingGet = _bufferBackend!.TryGetAsync(key, buffer, cancellationToken);
Expand All @@ -51,22 +55,22 @@ private ValueTask<T> GetBufferedBackendAsync<TState>(string key, TState state, F
// fast path; backend available immediately
if (pendingGet.GetAwaiter().GetResult())
{
var result = _serializer.Deserialize(new(buffer.GetCommittedMemory()));
var result = GetSerializer<T>().Deserialize(new(buffer.GetCommittedMemory()));
buffer.Dispose();
return new(result);
}

// fall back to main code-path, but without the pending bytes (we've already checked those)
return AwaitedBackend(this, key, state, callback, options, cancellationToken, buffer, default);

static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
static async ValueTask<T> AwaitedBackend(DistributedCache @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
CancellationToken cancellationToken, RecyclableArrayBufferWriter<byte> buffer, ValueTask<bool> pendingGet)
{
using (buffer)
{
if (await pendingGet)
{
return @this._serializer.Deserialize(new(buffer.GetCommittedMemory()));
return @this.GetSerializer<T>().Deserialize(new(buffer.GetCommittedMemory()));
}

var value = await callback(state, cancellationToken);
Expand All @@ -77,7 +81,7 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
else
{
buffer.Reset();
@this._serializer.Serialize(value, buffer);
@this.GetSerializer<T>().Serialize(value, buffer);
await @this._bufferBackend!.SetAsync(key, new(buffer.GetCommittedMemory()), options ?? _defaultOptions, cancellationToken);
}

Expand All @@ -86,7 +90,20 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
}
}

private ValueTask<T> GetLegacyBackendAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
private ICacheSerializer<T> GetSerializer<T>()
{
var obj = (ICacheSerializer<T>?)_services.GetService(typeof(ICacheSerializer<T>));
if (obj is null)
{
ThrowNoSerializer(typeof(T));
}
return obj!;

}

static void ThrowNoSerializer(Type type) => throw new InvalidOperationException("No serializer registered for " + type.FullName);

private ValueTask<T> GetLegacyBackendAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
{
var pendingBytes = _backend.GetAsync(key, cancellationToken);
if (!pendingBytes.IsCompletedSuccessfully)
Expand All @@ -98,21 +115,21 @@ private ValueTask<T> GetLegacyBackendAsync<TState>(string key, TState state, Fun
var bytes = pendingBytes.Result;
if (bytes is not null)
{
return new(_serializer.Deserialize(new ReadOnlySequence<byte>(bytes))!);
return new(GetSerializer<T>().Deserialize(new ReadOnlySequence<byte>(bytes))!);
}

// fall back to main code-path, but without the pending bytes (we've already checked those)
return AwaitedBackend(this, key, state, callback, options, cancellationToken, null);

static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
static async ValueTask<T> AwaitedBackend(DistributedCache @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
CancellationToken cancellationToken, Task<byte[]?>? pendingBytes)
{
if (pendingBytes is not null)
{
var bytes = await pendingBytes;
if (bytes is not null)
{
return @this._serializer.Deserialize(new ReadOnlySequence<byte>(bytes));
return @this.GetSerializer<T>().Deserialize(new ReadOnlySequence<byte>(bytes));
}
}

Expand All @@ -124,7 +141,7 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
else
{
using var writer = new RecyclableArrayBufferWriter<byte>();
@this._serializer.Serialize(value, writer);
@this.GetSerializer<T>().Serialize(value, writer);
await @this._backend.SetAsync(key, writer.ToArray(), options ?? _defaultOptions, cancellationToken);
}

Expand Down
6 changes: 3 additions & 3 deletions src/Caching/Caching/src/IDistributedCacheT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

namespace Microsoft.Extensions.Caching.Distributed;

public interface IDistributedCache<T>
public interface IAdvancedDistributedCache
{
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Does not cause ambiguity due to callback signature delta")]
ValueTask<T> GetAsync(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);
ValueTask<T> GetAsync<T>(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);

[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Does not cause ambiguity due to callback signature delta")]
ValueTask<T> GetAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);
ValueTask<T> GetAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);

ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);

Expand Down
10 changes: 5 additions & 5 deletions src/Caching/Caching/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#nullable enable
Microsoft.Extensions.Caching.Distributed.DistributedCacheServiceExtensions
Microsoft.Extensions.Caching.Distributed.IAdvancedDistributedCache
Microsoft.Extensions.Caching.Distributed.IAdvancedDistributedCache.GetAsync<T>(string! key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
Microsoft.Extensions.Caching.Distributed.IAdvancedDistributedCache.GetAsync<TState, T>(string! key, TState state, System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
Microsoft.Extensions.Caching.Distributed.IAdvancedDistributedCache.RefreshAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.Extensions.Caching.Distributed.IAdvancedDistributedCache.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.Extensions.Caching.Distributed.IBufferDistributedCache
Microsoft.Extensions.Caching.Distributed.IBufferDistributedCache.SetAsync(string! key, System.Buffers.ReadOnlySequence<byte> value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! options, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
Microsoft.Extensions.Caching.Distributed.IBufferDistributedCache.TryGetAsync(string! key, System.Buffers.IBufferWriter<byte>! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>
Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>.Deserialize(System.Buffers.ReadOnlySequence<byte> source) -> T
Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>.Serialize(T value, System.Buffers.IBufferWriter<byte>! target) -> void
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.GetAsync(string! key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.GetAsync<TState>(string! key, TState state, System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.RefreshAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
Microsoft.Extensions.Caching.Distributed.IDistributedCacheInvalidation
Microsoft.Extensions.Caching.Distributed.IDistributedCacheInvalidation.CacheKeyInvalidated -> System.Func<string!, System.Threading.Tasks.ValueTask>!
Microsoft.Extensions.Caching.Distributed.TypedDistributedCacheOptions
Expand Down
2 changes: 1 addition & 1 deletion src/Caching/Caching/test/CacheConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public async Task StatefulUsage(bool useCustomBackend)
}
}

public class SomeService(IDistributedCache<Foo> cache)
public class SomeService(IAdvancedDistributedCache cache)
{
private int _backendCalls;
public int BackendCalls => _backendCalls;
Expand Down

0 comments on commit 44e2984

Please sign in to comment.