Lazy async initialization for expiring objects

Β· 887 words Β· 5 minutes to read

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, which provides out of the box support for deferring the creation of a large or resource-intensive objects.

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 and support this scenario in a generic way.

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 back to its default value.

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.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP