Last active
January 2, 2024 19:09
-
-
Save somedeveloper00/976db5307e9cdec34b89847113bfacc8 to your computer and use it in GitHub Desktop.
Cacheable Operation in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// #define CACHEABLEOPERATION_VERBOSE | |
using System.Collections.Generic; | |
using System.Threading; | |
using System.Threading.Tasks; | |
#if CACHEABLEOPERATION_VERBOSE | |
using UnityEngine; | |
#endif | |
namespace SaeedBarari | |
{ | |
/// <summary> | |
/// Represents a base class for cacheable operations. Cacheable operations are operations that can be cached for a certain duration. | |
/// If the operation is requested again within the cache duration, the cached result is returned instead of performing the operation again. | |
/// Simply use <see cref="GetAsync(Targs)"/> to retrieve a cached result or generate a new result if not found in the cache. | |
/// You can use this class to implement your own cacheable operations. | |
/// </summary> | |
/// <typeparam name="Tresult">The type of the result.</typeparam> | |
/// <typeparam name="Targs">The type of the arguments.</typeparam> | |
public abstract class CacheableOperationBase<Tresult, Targs> | |
{ | |
/// <summary> | |
/// Dictionary that stores cached results for the CacheableOperation class. | |
/// The dictionary maps input arguments of type Targs to their corresponding results of type Tresult. | |
/// </summary> | |
private readonly Dictionary<Targs, Tresult> _cachedResults; | |
/// <summary> | |
/// Dictionary that stores ongoing generations of cacheable operations. | |
/// The key represents the type of the cacheable operation, and the | |
/// value represents the awaiter to wait for the operation to complete. (the <see cref="bool"/> is not important and not used) | |
/// </summary> | |
private readonly Dictionary<Targs, TaskCompletionSource<Tresult>> _ongoingGenerations = new(); | |
/// <summary> | |
/// The cancellation token source used for cancelling the operation. | |
/// </summary> | |
private CancellationTokenSource _cts = new(); | |
/// <summary> | |
/// duration to wait and invalidate the cache after (in seconds) | |
/// </summary> | |
protected abstract float CacheDuration { get; } | |
/// <summary> | |
/// Gets the capacity of the results for implementors to override. | |
/// </summary> | |
protected abstract int ResultsCapacity { get; } | |
public CacheableOperationBase() | |
{ | |
_cachedResults = new Dictionary<Targs, Tresult>(ResultsCapacity); | |
} | |
/// <summary> | |
/// Retrieves a cached result based on the provided arguments, or generates a new result if not found in the cache. | |
/// </summary> | |
/// <param name="args">The arguments used to retrieve or generate the result.</param> | |
/// <returns>The cached result if found, or a newly generated result.</returns> | |
public async Task<Tresult> GetAsync(Targs args) | |
{ | |
if (_cachedResults.TryGetValue(args, out var result)) | |
{ | |
#if CACHEABLEOPERATION_VERBOSE | |
Debug.LogFormat("<color=green>used cache</color> on \'{0}\'", args); | |
#endif | |
return result; | |
} | |
// wait for same ongoing generation | |
if (_ongoingGenerations.TryGetValue(args, out var wait)) | |
{ | |
// return the result of the ongoing generation | |
var ongoingResult = await wait.Task; | |
#if CACHEABLEOPERATION_VERBOSE | |
Debug.LogFormat("<color=green>used ongoing generation</color> on \'{0}\'", args); | |
#endif | |
return ongoingResult; | |
} | |
// declare new ongoing generation wait | |
var tcs = new TaskCompletionSource<Tresult>(); | |
_ongoingGenerations[args] = tcs; | |
// generate new result | |
var generateResult = await GenerateResult(args, _cts.Token); | |
// process result | |
if (!generateResult.success) | |
{ | |
#if CACHEABLEOPERATION_VERBOSE | |
Debug.LogWarningFormat("failed to generate result for \'{0}\'", args); | |
#endif | |
return default; | |
} | |
else | |
{ | |
#if CACHEABLEOPERATION_VERBOSE | |
Debug.LogFormat("<color=yellow>generated new result</color> for \'{0}\'", args); | |
#endif | |
_cachedResults.Add(args, generateResult.value); | |
// removing cache later | |
_ = Task.Run(async () => | |
{ | |
await Task.Delay((int)(CacheDuration * 1000)); | |
_cachedResults.Remove(args); | |
#if CACHEABLEOPERATION_VERBOSE | |
Debug.LogFormat("<color=red>removed cache</color> for \'{0}\'", args); | |
#endif | |
}); | |
// end wait | |
tcs.SetResult(generateResult.value); | |
_ongoingGenerations.Remove(args); | |
return generateResult.value; | |
} | |
} | |
/// <summary> | |
/// Invalidates the cache and cancels any ongoing operations. | |
/// </summary> | |
public void Invalidate() | |
{ | |
_cachedResults.Clear(); | |
_ongoingGenerations.Clear(); | |
_cts.Cancel(); | |
_cts = new(); | |
} | |
/// <summary> | |
/// Generates the result for the cacheable operation. | |
/// </summary> | |
/// <param name="args">The arguments for the operation.</param> | |
/// <param name="ct">The cancellation token.</param> | |
/// <returns>The generated result.</returns> | |
protected abstract Task<Result> GenerateResult(Targs args, CancellationToken ct); | |
/// <summary> | |
/// Represents the result of a cacheable operation. | |
/// </summary> | |
protected readonly struct Result | |
{ | |
/// <summary> | |
/// Gets a value indicating whether the operation was successful. | |
/// </summary> | |
public readonly bool success; | |
/// <summary> | |
/// Gets the result value. | |
/// </summary> | |
public readonly Tresult value; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="Result"/> struct with the specified success and result values. | |
/// </summary> | |
/// <param name="success">A value indicating whether the operation was successful.</param> | |
/// <param name="value">The result value.</param> | |
private Result(bool success, Tresult value) | |
{ | |
this.success = success; | |
this.value = value; | |
} | |
/// <summary> | |
/// Creates a new instance of the <see cref="Result"/> struct representing a successful operation with the specified result value. | |
/// </summary> | |
/// <param name="result">The result value.</param> | |
public static Result Success(Tresult result) => new(true, result); | |
/// <summary> | |
/// Creates a new instance of the <see cref="Result"/> struct representing a failed operation. | |
/// </summary> | |
public static Result Failure() => new(false, default); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
here's an example implementation: