For clarity, the code examples in this answer are pseudocode intended to demonstrate my recommendations for login flow and general structure. They are not intended to be syntactically-correct directly runnable examples; make sure return types etc. are valid and name the methods in accordance to whichever style guide you use.
// bad!
async Task<HttpResponseMessage> MakeRequest(...params...)
{
await LoginCheck();
return await RequestWithGlobalState(...params...);
}
private Token storedToken = Token.ExpiredToken;
// better!
async Task<HttpResponseMessage> MakeRequest(...params...)
{
var authToken = await GetAuthToken();
return await RequestWithoutGlobalState(authToken, ...params...);
}
Tokenasync Task<Token> GetAuthToken()
{
lock(tokenLock)
{
if (storedToken.expiryTimeExpiryTime >= currentTime)
{
storedToken = await FetchNewToken();
}
return storedToken;
}
}
private Token storedToken = Token.ExpiredToken;
async Task<HttpResponseMessage> MakeRequest(...params...)
{
var authToken = await GetAuthToken();
return await RequestWithoutGlobalState(authToken, ...params...);
}
Tokenasync Task<Token> GetAuthToken()
{
IDisposable rLock = await rwLock.ReaderLockAsync();
IDisposable wLock = null;
var currentTime = DateTime.UtcNow;
try
{
// note: DateTime comparisons are not atomic so don't do this when not locked
// we're in a reader lock here so it's okay
if (storedToken.expiryTimeExpiryTime >= currentTime)
{
// need to release reader lock before switching to write mode
rLock.Dispose();
// only one thread at a time can enter the writer lock, and all threads must be out of the reader locks
wLock = await rwLock.WriterLockAsync();
// check again here in case another thread has already refreshed while we were waiting on a writer lock
if (storedToken.expiryTimeExpiryTime >= currentTime)
{
storedToken = await FetchNewToken();
}
wLock.Dispose();
// re-acquire reader lock
rLock = await rwLock.ReaderLockAsync();
}
return storedToken;
}
finally
{
rLock?.Dispose();
wLock?.Dispose();
}
}