3
\$\begingroup\$

I have an API application with token authorization. A token is valid for one hour. Each business client has its own instance of the API application:

{"client_1": "http://localhost:1"},
...
{"client_n": "http://localhost:n"}

In another application I have a class ApiService to make HTTP requests to the API. I create one instance of ApiService. Requests happen concurrently and I want to be sure that for each client token will be issued only once. I use Flurl to make HTTP requests.

public class TokenDetails
{
    public string Token { get; set; }
    public DateTime Expires { get; set; }
}

public class ClientDetails
{
    public string ClientId { get; set; }
    public string BaseAddress { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

public class ClientService
{
    private readonly ConcurrentDictionary<string, Task<TokenDetails>> _tokens =
        new ConcurrentDictionary<string, Task<TokenDetails>>();

    private readonly ConcurrentDictionary<string, object> _locks =
        new ConcurrentDictionary<string, object>();

    public async Task<string> GetDataAsync(ClientDetails client)
    {
        var token = await GetTokenAsync(client).ConfigureAwait(false);

        return await new Url(client.BaseAddress).AppendPathSegment("/data")
            .WithOAuthBearerToken(token.Token)
            .GetStringAsync().ConfigureAwait(false);
    }

    private Task<TokenDetails> GetTokenAsync(ClientDetails client)
    {
        //try find valid token task
        if (!_tokens.TryGetValue(client.ClientId, out var tokenTask) 
            || tokenTask.IsFaulted 
            || tokenTask.IsCanceled 
            || (tokenTask.IsCompleted && tokenTask.Result.Expires < DateTime.UtcNow))
        {
            //token task not found or token is expired - enter lock to make http call
            var lockObject = _locks.GetOrAdd(client.ClientId, id => new object());
            lock (lockObject)
            {
                //try find valid token task again after aquiring lock
                if (!_tokens.TryGetValue(client.ClientId, out tokenTask) 
                    || tokenTask.IsFaulted 
                    || tokenTask.IsCanceled 
                    || (tokenTask.IsCompleted && tokenTask.Result.Expires < DateTime.UtcNow))
                {
                    //http call
                    tokenTask = new Url(client.BaseAddress).AppendPathSegment("/login")
                        .WithBasicAuth(client.Username, client.Password)
                        .GetJsonAsync<TokenDetails>();
                    _tokens.AddOrUpdate(client.ClientId, key => tokenTask, (key, oldToken) => tokenTask);
                }

            }
        }

        return tokenTask;
    }
}
\$\endgroup\$
1
\$\begingroup\$

The pattern looks correct. One disadvantage here is you're blocking threads waiting for the lock. You could instead use a SemaphoreSlim and its WaitAsync method for a nonblocking lock.

Even better, have a look at LazyCache. (Scott Hanselman blogged about this recently.) It basically does all of this for you - caching, asynchronous double-check locking, and guaranteeing refresh only happens once. You could replace most of what you've done here with a GetTokenAsync implementation that looks something like this:

return await _tokenCache.GetOrAddAsync(
    $"tokens-{client.ClientId}",
    () => client.BaseAddress
        .AppendPathSegment("/login")
        .WithBasicAuth(client.Username, client.Password)
        .GetJsonAsync<TokenDetails>(),
    TimeSpan.FromHours(1));

One little caveat here is you need to know ahead of time how long to cache the token, i.e. you can't use the Expires value returned from the server. But in your case it sounds like you know it's an hour, so hopefully that's a non-issue.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.