AF.Decay 2.0.0

dotnet add package AF.Decay --version 2.0.0
                    
NuGet\Install-Package AF.Decay -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AF.Decay" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AF.Decay" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AF.Decay" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AF.Decay --version 2.0.0
                    
#r "nuget: AF.Decay, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AF.Decay@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AF.Decay&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AF.Decay&version=2.0.0
                    
Install as a Cake Tool

C# Decay<T>

NuGet CI

Wrap any object so its value expires once a condition is met: after a number of accesses, after a point in time, or when your own rule says so.

dotnet add package AF.Decay

Targets .NET 10 and .NET Standard 2.0.

Things to Note

  • Policies you don't configure never apply. If you configure several, the first one to be met expires the value.
  • Expiration is permanent. Once expired, the wrapped value is released and never comes back.
  • Instances are thread-safe, and an access limit is honored exactly under concurrency.
  • Every read of Value, TryGetValue or GetValueOrDefault counts as one access. Reading Expired, AccessCount and similar properties does not.
  • Decay<T> instances compare by reference, not by the value they wrap.

Getting Started

Suppose you want someObject to expire after it has been accessed five times or after 10 seconds, whichever comes first:

var decaying = Decay.Of(someObject)
    .ExpireAfterAccesses(5)
    .ExpireAfter(TimeSpan.FromSeconds(10))
    .Build();

if (decaying.TryGetValue(out var value))
{
    // Still alive: use value.
}

After expiry, Value returns default(T). To get an exception instead, add .ThrowOnExpiration():

var decaying = Decay.Of(someObject).ExpireAfterAccesses(1).ThrowOnExpiration().Build();

_ = decaying.Value; // OK
_ = decaying.Value; // throws ObjectExpiredException; ex.Reason == ExpirationReasons.Count

Reading the Value

Member On expiry
TryGetValue(out T value) returns false
GetValueOrDefault(T fallback) returns fallback
Value returns default(T), or throws if created with ThrowOnExpiration()

Prefer TryGetValue when default(T) (0, false, null) is a meaningful value.

Policies

Expire After Accessing x Times

var decaying = Decay.OnCount(someObject, 5);
// The first 5 reads return the value; the 6th returns default.

Expire After a Time Horizon

var relative = Decay.OnExpiration(someObject, TimeSpan.FromSeconds(10));
var absolute = Decay.OnExpiration(someObject, DateTimeOffset.UtcNow.AddSeconds(10));

Expire On a Custom Condition

The condition receives a DecayState<T> holding the value, AccessCount, AccessLimit, ExpiresAt and Now, and returns true to expire. It runs on every access and every read of Expired until the value expires, so keep it fast and free of side effects.

var cart = new List<string> { "apple" };
var decaying = Decay.OnCondition(cart, state => state.Value.Count == 0);

Combining Policies

var session = Decay.Of(sessionToken)
    .ExpireAfterAccesses(100)
    .ExpireAt(endOfShift)
    .ExpireWhen(state => state.Now.DayOfWeek == DayOfWeek.Sunday)
    .ThrowOnExpiration()
    .Build();

A builder also converts implicitly: Decay<string> session = Decay.Of(sessionToken).ExpireAfterAccesses(100);

Inspecting and Controlling

bool expired = decaying.Expired;
ExpirationReasons why = decaying.ExpirationReason; // None, Count, Time, Condition, Manual (flags)
long reads = decaying.AccessCount;                 // reads so far
long? limit = decaying.AccessLimit;                // configured limit, or null
long? left = decaying.RemainingAccesses;           // reads left, or null
DateTimeOffset? expiresAt = decaying.ExpiresAt;    // configured expiry, or null
decaying.Expire();                                 // expire right now

Testing Time-Based Expiry

Inject a TimeProvider, for example FakeTimeProvider from Microsoft.Extensions.TimeProvider.Testing:

var time = new FakeTimeProvider();
var decaying = Decay.Of("token").ExpireAfter(TimeSpan.FromMinutes(5)).UseTimeProvider(time).Build();

time.Advance(TimeSpan.FromMinutes(6));
// decaying.Expired == true

Combine with Lazy<T>

Delay creating an object and also make it decay. The order in which you nest Lazy<> and Decay<> determines the behavior.

// The expensive object is created on first use; its 10-second lifetime starts then.
var delayedDecaying = new Lazy<Decay<SomeObject>>(() => Decay.OnExpiration(new SomeObject(), TimeSpan.FromSeconds(10)));

// The lifetime starts now, even though SomeObject isn't created yet.
// Useful when an expensive object should never be created if it isn't needed within the window.
var decayingLazy = Decay.OnExpiration(new Lazy<SomeObject>(() => new SomeObject()), TimeSpan.FromSeconds(10));

Collections

Sorting puts expired items first, then items with the fewest remaining accesses, then the earliest expiration time. Custom conditions don't affect order.

var list = new List<Decay<int>>();
// ... add some objects
list.Sort(); // Ordered by next to decay

Extension methods for collections of Decay<T>:

// Next item due to decay, removing expired items (if the collection isn't read-only)
var next = list.GetNextDecayingItem();

// Same, but leave expired items in place
var nextKeep = list.GetNextDecayingItem(clearDecayedItems: false);

var expired = list.GetExpired();     // snapshot of expired items
var active = list.GetActive();       // snapshot of active items
int removed = list.RemoveExpired();  // remove expired items, returns count removed

Upgrading from 1.x

2.0 keeps the 1.x API compiling, with these changes:

  • Reference equality. ==, Equals and GetHashCode no longer compare the wrapped values. The 1.x behavior caused collection helpers to remove the wrong item.
  • default(T) is a real value. new Decay<int>(0) is no longer born expired.
  • Expiration is permanent, including when throwing. A custom condition flipping back to false no longer revives the value.
  • Access counting happens on every read, not only when an access limit is set.
  • Sort order compares remaining accesses, then expiration time. It no longer mixes seconds with access counts.
  • Exception messages no longer include the wrapped value.
  • Deprecated: GetDecayedItems → GetExpired, GetNonDecayedItems → GetActive, ClearDecayedItems → RemoveExpired, and the (DateTimeOffset?, long?) condition overload → Decay.OnCondition(value, state => ...). These will be removed in 3.0 (#4).
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 89 10/13/2026
1.4.0 546 12/20/2024
1.3.0 7,399 6/15/2023
1.2.0 422 6/9/2023
1.1.0 414 6/8/2023
1.0.0 573 2/28/2022

2.0.0: Fixes default(T) being treated as expired, collection helpers removing the wrong item, non-permanent expiration, unit-mixing sort order and value leakage in exception messages. Adds Decay.Of(...) builder, type-inferred Decay.OnCount/OnExpiration/OnCondition, TryGetValue, GetValueOrDefault, Expire(), ExpirationReason, access/expiry state properties and TimeProvider support. Targets netstandard2.0 and net10.0. See the README's "Upgrading from 1.x" section for behavior changes.