Luke Vo
Luke Vo

Reputation: 20668

How do I properly dispose expired objects in MemoryCache (it may still be in used)?

I understand items in MemoryCache are not disposed when expired. I am caching some X509Certificate2 which according to the documentation, should be disposed when done.

However, my naive approach would dispose the object when the object may still be used by some threads (see code below).

How do I correctly handle this case? I think I may need a reference count or something similar?

await cache.GetOrCreateAsync("IdTokenCerts", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = JwtCertsCacheLifetime;
            entry.RegisterPostEvictionCallback((_, value, _, _) =>
            {
                if (value is IEnumerable<SecurityKey> keys)
                {
                    foreach (var key in keys)
                    {
                        if (key is X509SecurityKey x509Key)
                        {
                            x509Key.Certificate.Dispose();
                        }
                    }
                }
            });

            // ...
        }

Upvotes: 0

Views: 110

Answers (1)

Luke Vo
Luke Vo

Reputation: 20668

I implemented a simple tracker like this:

public class UsageTracker<TData>(TData data)
{

    public event EventHandler OnDisposed = delegate { };

    public bool DisposeRequested { get; private set; }

    public TData Data => data;
    public int UsageCount { get; private set; }

    public void RequestDispose()
    {
        if (DisposeRequested) { return; }

        DisposeRequested = true;
        CheckForDispose();
    }

    public UsageTrackerItem<TData> RequestUsage()
    {
        if (DisposeRequested)
        {
            throw new ObjectDisposedException(nameof(UsageTracker<TData>));
        }

        UsageCount++;
        return new UsageTrackerItem<TData>(this, data);
    }

    internal void OnItemDisposed()
    {
        UsageCount--;
        CheckForDispose();
    }

    void CheckForDispose()
    {
        if (UsageCount == 0 && DisposeRequested)
        {
            OnDisposed(this, EventArgs.Empty);
        }
    }

}

public class UsageTrackerItem<TData> : IDisposable
{
    readonly UsageTracker<TData> tracker;
    public TData Data { get; private set; }

    internal UsageTrackerItem(UsageTracker<TData> tracker, TData data)
    {
        this.tracker = tracker;
        Data = data;
    }

    public void Dispose()
    {
        tracker.OnItemDisposed();
    }
}

Usage:

var data = 5;
var tracker = new UsageTracker<int>(data);

// Test tracker
tracker.OnDisposed += (sender, e) => Console.WriteLine("Code for disposing items should be here");

var task = Task.WhenAll(Enumerable.Range(0, 10).Select(async i =>
{
    using var data = tracker.RequestUsage();
    await Task.Delay(Random.Shared.Next(1000, 5000));

    Console.WriteLine($"Task {i} completed. {tracker.UsageCount} usages remaining.");
}));

tracker.RequestDispose();
Console.WriteLine("Tracker disposal requested");

try
{
    using var staleData = tracker.RequestUsage();
}
catch (Exception ex)
{
    Console.WriteLine("There should be this exception due to requesting data after requesting disposal:");
    Console.WriteLine(ex.Message);
}

await task;

Output:

Tracker disposal requested
There should be this exception due to requesting data after requesting disposal:
Cannot access a disposed object.
Object name: 'UsageTracker'.
Task 7 completed. 10 usages remaining.
Task 2 completed. 9 usages remaining.
Task 0 completed. 8 usages remaining.
Task 9 completed. 7 usages remaining.
Task 1 completed. 6 usages remaining.
Task 5 completed. 6 usages remaining.
Task 8 completed. 4 usages remaining.
Task 4 completed. 3 usages remaining.
Task 6 completed. 2 usages remaining.
Task 3 completed. 1 usages remaining.
Code for disposing items should be here

Upvotes: 1

Related Questions