Allow Scoped values that do not implement IDisposable
I would like to use the ScopedCache as a "While Scoped values are in use it will not be removed from the cache. The value does not have to be IDisposable." Is there a reason for the IDisposable restraint?
I would assume the value could be checked to see if it implements IDisposable and dispose the value if necessary. Of Course, I could create a Disposable wrapper, but Lifetime is already a wrapper I Would prefer not to create yet another wrapper if the constraint is no necessary.
Scoped values are immediately removed from the cache by the eviction policy or by calling TryRemove - scoping does not pin objects in the cache or affect cache eviction. Scoping controls when Dispose is called on the cached value. It does not control when the value is evicted. In other words, using scoped with non-IDisposable objects would have no effect - the scoping mechanism exists purely to call Dispose, if there is no Dispose method scoping becomes a nop.
This is the sequence diagram from the wiki page for IDisposable and Scoped values, notice that in the middle at step 11, the object is immediately removed from the cache when TryRemove is called:
sequenceDiagram
autonumber
participant Thread A
participant Thread B
participant Thread C
participant Cache
participant Scope
participant Lifetime Cache
participant Lifetime A
participant Lifetime B
Thread A->>Cache: A calls ScopedGetOrAdd
Cache->>Scope: create scope
activate Scope
Scope-->>Object: create object
activate Object
Cache-->Lifetime Cache: cache holds lifetime
activate Lifetime Cache
Cache-->Lifetime A: creates A's lifetime
activate Lifetime A
Lifetime A-->> Thread A: A holds lifetime
Thread B->>Cache: B calls ScopedGetOrAdd
Cache-->Lifetime B: creates B's lifetime
activate Lifetime B
Lifetime B-->> Thread B: B holds lifetime
Thread C->>Cache: C calls TryRemove
Cache--xLifetime Cache: Cache removes the object and disposes lifetime
deactivate Lifetime Cache
Thread A->>Object: Thread A uses the object
Lifetime A->>Thread A: A disposes lifetime
Lifetime A--x Scope: lifetime de-refs scope
deactivate Lifetime A
Thread B->>Object: Thread B uses the object
Lifetime B->>Thread B: B disposes lifetime
Lifetime B--x Scope: B de-refs scope
deactivate Lifetime B
Scope--xObject: dispose object
deactivate Object
deactivate Scope
Is there a reason for the IDisposable restraint?
Scoped caches exist purely to prevent the cache calling dispose on the cached value while other callers are still using the object after retrieving from the cache. The design rationale for scoped assumes that the objects stored in the cache are in one of two categories:
- Cached object is always in a valid state and cannot be marked as invalid. The common case is that cached objects are immutable and are valid for use even after the cache has evicted them. In other words, if an object is always in a valid state, it is transparent to the user of the object, having retrieved it from the cache, whether it is still cache resident while they are using it- it simply doesn't matter, because cache eviction does not mutate the object.
- Cached object uses native or pooled resources, or some other category of behavior that requires cleanup. After cleanup, the object is no longer valid for use by any caller. In .NET, this is typically implemented via the
IDisposablecontract, where callingDisposereleases resources or whatever and puts an object into an invalid state (henceObjectDisposedExceptionis thrown when using disposed objects).
Scoped caches solve case 2, and guarantee safety by preventing disposed objects from being returned/keeping objects alive during use/promptly cleaning up after use when all callers finish using objects. The wiki page for IDisposable and Scoped values has a code sample that illustrates a data race caused by incorrect object disposal.