Skip to content

Instantly share code, notes, and snippets.

@somedeveloper00
Last active January 2, 2024 19:09
Show Gist options
  • Save somedeveloper00/976db5307e9cdec34b89847113bfacc8 to your computer and use it in GitHub Desktop.
Save somedeveloper00/976db5307e9cdec34b89847113bfacc8 to your computer and use it in GitHub Desktop.
Cacheable Operation in C#
// #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);
}
}
}
@somedeveloper00
Copy link
Author

somedeveloper00 commented Dec 18, 2023

here's an example implementation:

/// <summary>
/// Represents an operation to retrieve a player's profile from PlayFab.
/// </summary>
public sealed class PlayfabOp_GetProfile : CacheableOperationBase<DatabaseProfileModel,
    (PlayFabAuthenticationContext authContext, DatabaseUserModel usermodel, string playfabId)>
{
    protected override float CacheDuration => 120; // 2 minutes

    protected override int ResultsCapacity => 128; // reserving enough memory for 128 cached profiles

    protected override async Task<Result> GenerateResult(
        (PlayFabAuthenticationContext authContext, DatabaseUserModel usermodel, string playfabId) args,
        CancellationToken ct)
    {
        // if its self, dont send request and waste time
        if (args.playfabId == args.usermodel.token && args.usermodel.profile != null)
        {
            return Result.Success(new DatabaseProfileModel()
            {
                iconSpriteIndex = args.usermodel.profile.iconSpriteIndex
            });
        }


        var tcs = new TaskCompletionSource<DatabaseProfileModel>();

        PlayFabClientAPI.GetUserReadOnlyData(new()
        {
            AuthenticationContext = args.authContext,
            PlayFabId = args.playfabId,
            Keys = new() { "profile" }
        },
        result =>
        {
            if (ct.IsCancellationRequested)
            {
                tcs.SetResult(null);
                return;
            }

            try
            {
                var profile = new DatabaseProfileModel();
                JsonConvert.DeserializeObject<ProfileDTO>(result.Data["profile"].Value).Apply(profile);
                tcs.SetResult(profile);
            }
            catch (Exception ex)
            {
                tcs.SetResult(null);
                Debug.LogException(ex);
            }
        },
        error =>
        {
            tcs.SetResult(null);
            Debug.LogWarningFormat("fetching profile {0} failed:{1}", args.playfabId, error.GenerateErrorReport());
        });

        var profile = await tcs.Task;

        return profile != null ? Result.Success(profile) : Result.Failure();
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment