I have to develop a small security management application that relies on a Web API. Based on that API, I have generated the REST client classes (Open API generator), but I still have to code on top to provide things like retrials and token management (avoid getting a token for each call).
Code
///<inheritdoc/>
public class DataLakeSecurityApiTokenService : IDataLakeSecurityApiTokenService
{
private const int TokenRefreshCoolDownPeriod = 3600; // seconds
private static readonly SemaphoreSlim NewTokenMutex = new SemaphoreSlim(1, 1);
private static string Token { get; set; }
private static readonly Stopwatch StopWatch= new Stopwatch();
private IConfigurationService ConfigurationService { get; }
private ILoggingService Logger { get; }
private async Task RefreshToken()
{
var defaultApiClient = new DefaultApi(ConfigurationService.DataLakeBasePath);
var tokenObj = (JObject)await defaultApiClient.LoginTokenPostAsync(
ConfigurationService.DataLakeUser, ConfigurationService.DataLakePassword);
Token = tokenObj["access_token"].ToString();
StopWatch.Restart();
}
///<inheritdoc/>
public DataLakeSecurityApiTokenService(IConfigurationService configurationService, ILoggingService logger)
{
ConfigurationService = configurationService;
Logger = logger;
}
///<inheritdoc/>
public async Task<ValidationResult<string>> GetLoginToken(bool force = false)
{
await NewTokenMutex.WaitAsync();
try
{
bool expired = StopWatch.Elapsed.Seconds > TokenRefreshCoolDownPeriod;
if (expired || force || string.IsNullOrWhiteSpace(Token))
await RefreshToken();
return new ValidationResult<string> {Payload = Token};
}
catch (ApiException exc)
{
Logger.LogError(exc, "Failed to get login token");
return new ValidationResult<string> { IsError = true, Message = exc.Message};
}
finally
{
NewTokenMutex.Release();
}
}
///<inheritdoc/>
public async Task<ValidationResult<TRes>> ExecuteWithTokenRefresh<TApiClient, TRes>(
string context, string functionName, params object[] functionParams)
{
for (int retryIndex = 0; retryIndex < 2; retryIndex++)
{
var tokenRes = await GetLoginToken(retryIndex > 0);
if (tokenRes.IsError)
return ValidationResult<TRes>.Create(tokenRes);
try
{
var defaultConfiguration = GetDefaultConfiguration(tokenRes.Payload);
var apiClient = Activator.CreateInstance(typeof(TApiClient), args: defaultConfiguration);
// ReSharper disable once PossibleNullReferenceException
var apiResponse = await (Task<ApiResponse<TRes>>) typeof(TApiClient).GetMethod(functionName)
?.Invoke(apiClient, functionParams);
// ReSharper disable once PossibleNullReferenceException
return new ValidationResult<TRes> {Payload = apiResponse.Data};
}
catch (ApiException exc)
{
Logger.LogError(exc, $"{context} error, retry index = {retryIndex}");
if (retryIndex > 0)
return new ValidationResult<TRes> { IsError = true, Message = exc.Message };
}
}
// normally it should not ever reach this point
return new ValidationResult<TRes>();
}
//TODO: move to a more appropriate place when a common service is defined
///<inheritdoc/>
public Configuration GetDefaultConfiguration(string token)
{
return new Configuration
{
BasePath = ConfigurationService.DataLakeBasePath,
AccessToken = token
};
}
}
This code does the following:
- caches the last valid token for a period
- provides a way to quickly execute an API client method without worrying about getting the token, by fetching the token if necessary and also performing a retrial if an error has occurred. Unfortunately the generated code does not allow to discriminate between access denied errors (invalid or expired token) and a real internal server error (500), so I have to retry regardless of the error to be certain I catch access denied errors.
Usage
///<inheritdoc/>
public async Task<ValidationResult<List<Tag>>> GetTableLevelTagList()
{
var ret = await DataLakeSecurityApiTokenService.ExecuteWithTokenRefresh<SecurityTagsApi, List<Tag>>(
"Get table level tag list",
nameof(SecurityTagsApi.ListTableLevelSecurityTagsTagTableGetAsyncWithHttpInfo));
return ret;
}
The usage is pretty straightforward, but what I particularly dislike is that I am using dynamic invocation. If the consumer works with nameof
it is pretty safe, but it still quite messy when working with methods with parameters which are not validated at compile-time.
I am wondering if there is way to improve this code.