Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cache an IDisposable object

Suppose I have this class:

public class StreamEntity : IPersistableEntity, IDisposable
{
     public Stream Stream { get; set; }

     public void Dispose()
     {
           if(Stream != null) Stream.Dispose();
     }
}

Now let's say I want to store an instance of this class in some kind of cache, like MemoryCache.

After storing the entity in the cache, I proceed to dipose it.

After some time, a consumer tries to retrieve the same entity so I fetch it from the cache, but there is a little problem... The object is disposed, so I cannot read its Stream property (Remember we disposed it after caching it?)

Now, one way to solve this, is to never dispose the cached entities. This seems logical since they should be kept alive in memory, but what happens after expiration?

Specifically, when MemoryCache automatically removes an object that has expired, will it dispose it?

According to Will MemoryCache dispose IDisposable items when evicted?, MemoryCache will not dispose its cached items.

Knowing this, How can I cache a disposable object? When should I dispose the entities if I'm not in control of their eviction?

like image 801
Matias Cicero Avatar asked Sep 21 '15 23:09

Matias Cicero


2 Answers

You are correct that MemoryCache does not call Dispose, however you can tell it to call Dispose when evicting a item.

static void Main(string[] args)
{
    var policy = new CacheItemPolicy
    {
        RemovedCallback = RemovedCallback,
        SlidingExpiration = TimeSpan.FromMinutes(5)
    };
    Stream myStream = GetMyStream();
    MemoryCache.Default.Add("myStream", myStream, policy);
}

private static void RemovedCallback(CacheEntryRemovedArguments arg)
{
    if (arg.RemovedReason != CacheEntryRemovedReason.Removed)
    {
        var item = arg.CacheItem.Value as IDisposable;
        if(item != null)
            item.Dispose();
    }
}

The above example creates a Stream object and if it is unused for 5 minutes it will have Dispose() called on it. If the stream is removed due to a Remove( call removing the item or a Set( call overwriting the item it will not have Dispose() called on it.

like image 114
Scott Chamberlain Avatar answered Nov 13 '22 10:11

Scott Chamberlain


The first thing to consider is whether it's a good idea to cache such an item at all. Many disposable objects hold onto relatively limited resources, and/or some which will time-out in some way. These do not cache well, and it's best just not to do so.

On the other hand, some disposable objects don't really need to be disposable, but they share a base class with many that do, or implement an interface that needs to allow for disposal at a particular point if it is done (IEnumerator<T>) and so you could know that it's actually fine to not dispose it at all. In such a case you can cheerfully ignore the issue, but be careful of changes in implementation with later versions, unless the Dispose() is explicitly documented as safe to ignore.

Yet other possibility is to cache something that allows for quicker construction of an object, which is the approach I'd recommend with Stream: Don't cache Stream objects at all, but rather cache the bytes that could be read from it. When calling code wants to read the stream first construct a new MemoryStream with that byte array as the buffer. If the stream can be accessed from outside the assembly wrap that stream in another stream that enforces a read-only policy (if it is only accessible inside your own code you can skip that as an optimisation, by just being careful never to write to the stream). Then return that stream. The calling code can treat it like a stream obtained any other way (including calling Dispose() when it's done) but you can still give the calling code those stream faster because of the caching.

like image 45
Jon Hanna Avatar answered Nov 13 '22 09:11

Jon Hanna