Today I wanted to share something I found myself using quite a lot recently, and that is not supported out of the box by the .NET framework.
So, as part of the framework, we have Lazy
However, what if the object requires async operation to be created, and what if its value expires after some time, and it needs to be recomputed? Let’s have a look at how to solve this.
Sample use case π
Let’s imagine the following scenario - you are making service calls to an HTTP API using a typical OAuth 2.0 client credentials flow. This means, you need to obtain the token from the identity server in order to be able to use it to call the resource server. Typically you’d only fetch the token once it’s needed, and the operation of retrieving the token should be async, as it’s network bound. Additionally, according to the OAuth spec, client credentials flow doesn’t support refresh tokens, so once the original access token expires, you’ll need to re-request a new one.
All of that means that we are dealing with a lazy initialization (get the token only once we need it for the first time), with an async operation (network bound) and with an object that will expire (access token has a limited lifetime).
Let’s call our construct AsyncExpiringLazy
Building an AsyncExpiringLazy π
In addition to the construct itself, we will also need a simple wrapper around the result object and its expiration timestamp, so let’s start there.
public struct ExpirationMetadata<T>
{
public T Result { get; set; }
public DateTimeOffset ValidUntil { get; set; }
}
So far so good - not much to explain there. Next let’s add AsyncExpiringLazy. Since properties can’t be async in C#, in our AsyncExpiringLazy, we’ll need to use an instance method as a way to fetch our underlying value.
Let’s first create the outline of the class, and then fill in the remaining blanks.
public class AsyncExpiringLazy<T>
{
public AsyncExpiringLazy(Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> valueProvider)
{
// TODO
}
public Task<bool> IsValueCreated()
{
// TODO
}
public async Task<T> Value()
{
// TODO
}
public Task Invalidate()
{
// TODO
}
}
So the constructor will take in a value provider - the provider itself will receive the “previous/old”, expired (or expiring) value and will be responsible of providing a new one - wrapped in the ExpirationMetadata defining it’s expiration time.
Filling in the remaining blanks is surprisingly easy - it’s shown below. The only “trick” in the code is to use SemaphoreSlim for locking - since C# does not allow traditional lock statements to contain awaits.
Regarding invalidation, we can just reset the ExpirationMetadata
The code is shown below.
public class AsyncExpiringLazy<T>
{
private static readonly SemaphoreSlim SyncLock = new SemaphoreSlim(initialCount: 1);
private readonly Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> _valueProvider;
private ExpirationMetadata<T> _value;
public AsyncExpiringLazy(Func<ExpirationMetadata<T>, Task<ExpirationMetadata<T>>> valueProvider)
{
if (valueProvider == null) throw new ArgumentNullException(nameof(valueProvider));
_valueProvider = valueProvider;
}
private bool IsValueCreatedInternal => _value.Result != null && _value.ValidUntil > DateTimeOffset.UtcNow;
public async Task<bool> IsValueCreated()
{
await _syncLock.WaitAsync().ConfigureAwait(false);
try
{
return IsValueCreatedInternal;
}
finally
{
_syncLock.Release();
}
}
public async Task<T> Value()
{
await _syncLock.WaitAsync().ConfigureAwait(false);
try
{
if (IsValueCreatedInternal)
{
return _value.Result;
}
}
finally
{
_syncLock.Release();
}
await _syncLock.WaitAsync().ConfigureAwait(false);
try
{
var result = await _valueProvider(_value).ConfigureAwait(false);
_value = result;
return _value.Result;
}
finally
{
_syncLock.Release();
}
}
public async Task Invalidate()
{
await _syncLock.WaitAsync().ConfigureAwait(false);
_value = default(ExpirationMetadata<T>);
_syncLock.Release();
}
}
And that’s it.
Usage π
Usage is quite simple and is shown in the next unit test:
// helper class for illustration
public class TokenResponse
{
public string AccessToken { get; set; }
}
[Fact]
public async Task End2End()
{
var testInstance = new AsyncExpiringLazy<TokenResponse>(async metadata =>
{
await Task.Delay(1000);
return new ExpirationMetadata<TokenResponse>
{
Result = new TokenResponse
{
AccessToken = Guid.NewGuid().ToString()
}, ValidUntil = DateTimeOffset.UtcNow.AddSeconds(2)
};
});
// 1. check if value is created - shouldn't
Assert.False(await testInstance.IsValueCreated());
// 2. fetch lazy expiring value
var token = await testInstance.Value();
// 3a. verify it is created now
Assert.True(await testInstance.IsValueCreated());
// 3b. verify it is not null
Assert.NotNull(token.AccessToken);
// 4. fetch the value again. Since it's lifetime is 2 seconds, it should be still the same
var token2 = await testInstance.Value();
Assert.Same(token, token2);
// 5. sleep for 2 seconds to let the value expire
await Task.Delay(2000);
// 6. fetch again
var token3 = await testInstance.Value();
// 7. verify we now have a new (recreated) value - as the previous one expired
Assert.NotSame(token2, token3);
// 8. invalidate the value manually before it has a chance to expire
await testInstance.Invalidate();
// 9. check if value is created - shouldn't anymore
Assert.False(await testInstance.IsValueCreated());
}
You can create an instance of AsyncExpiringLazy at any point - for example it could be a field in some class of yours. You will need to pass in a delegate responsible for value creation - it will give you a chance to inspect the old value too if needed.
From there on, it’s all about accessing the value whenever you need it. And if it expires, AsyncExpiringLazy will re-create it on next access.
All the code for this article is located here on Github as .NET Standard 1.3 library. Hope this helps in some way.