-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Description
Type of issue
Other (describe below)
Description
The class CacheSignal<T> should signal that the cache is populated before accessing it.
Calls and waits for the _cacheSignal.WaitAsync() to release, this ensures that the cache is populated before accessing the cache.
The semaphore of the CacheSignal is already released in its initial state, it should be created with new (0, 1) instead of new (1, 1) otherwise the PhotoService may access an unpopulated cache.
namespace CachingExamples.Memory;
public sealed class CacheSignal<T>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <summary>
/// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
/// When signaled (consumer calls <see cref="Release"/>), the
/// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
/// </summary>
public Task WaitAsync() => _semaphore.WaitAsync();
/// <summary>
/// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
/// Callers who were waiting, will be able to continue.
/// </summary>
public void Release() => _semaphore.Release();
}Suggestion
Additionally I would suggest a refactor of the CacheSignal<T> as it should only be released from the CacheWorker once the cache is populated. The PhotoService should not need that mutual access to the cache.
CacheSignal<T> should utilize a TaskCompletionSource instead of a SemaphoreSlim.
namespace CachingExamples.Memory;
public sealed class CacheSignal<T>
{
private readonly TaskCompletionSource _tcs = new();
/// <summary>
/// Exposes a <see cref="Task"/> that represents the asynchronous wait operation for the population of the cache.
/// When signaled (consumer calls <see cref="Release"/>), the
/// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
/// </summary>
public Task WaitForPopulateAsync() => _tcs.Task;
/// <summary>
/// Exposes the ability to signal the release of the <see cref="WaitForPopulateAsync"/>'s operation to indicate that the cache has been populated.
/// This only needs to be called once after population of the cache.
/// Callers who were waiting, will be able to continue.
/// </summary>
public void Release() => _tcs.TrySetResult();
}The PhotoService should just wait for the population of the cache and not release it anymore. The cache itself is thread safe, therefore there is no need for the mutual access due to the previous behavior of the CacheSignal<T>.
using Microsoft.Extensions.Caching.Memory;
namespace CachingExamples.Memory;
public sealed class PhotoService(
IMemoryCache cache,
CacheSignal<Photo> cacheSignal,
ILogger<PhotoService> logger)
{
public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
{
await cacheSignal.WaitForPopulateAsync(); // wait until the cache is populated
Photo[] photos =
(await cache.GetOrCreateAsync(
"Photos", _ =>
{
logger.LogWarning("This should never happen!");
return Task.FromResult(Array.Empty<Photo>());
}))!;
// If no filter is provided, use a pass-thru.
filter ??= _ => true;
foreach (Photo photo in photos)
{
if (!default(Photo).Equals(photo) && filter(photo))
{
yield return photo;
}
}
}
}Page URL
https://learn.microsoft.com/en-us/dotnet/core/extensions/caching
Content source URL
https://github.com/dotnet/docs/blob/main/docs/core/extensions/caching.md
Document Version Independent Id
07beacc6-ddcf-2fd5-b896-00f999764b00
Platform Id
97e770aa-0b26-d69d-80e4-578056090645
Article author
Metadata
- ID: ed120c7a-f4c0-c1d1-cb8c-168a3eba6dd9
- PlatformId: 97e770aa-0b26-d69d-80e4-578056090645
- Service: dotnet-fundamentals