1
\$\begingroup\$

I wrote a method that lets me retry RestSharp requests when a specific exception is thrown.
Here's the code for that method:

private async Task<T_Out> RunRequestWithRetry<T_In, T_Out>(Func<SignatureAccount, T_In, T_Out> request, SignatureAccount signatureAccount, T_In requestBody) where T_Out : Task
{
    try
    {
        var result = request.Invoke(signatureAccount, requestBody) as Task;
        await result;
        return result as T_Out;
    }
    catch (AMAApiException<ErrorResult> apiEx)
    {
        if (apiEx.ErrorType != AMAErrorType.TokenNeedsRefresh)
            throw apiEx;

        // Refresh Tokens
        var signatureAccountService = new SignatureAccountService(MvcApplication.Config);
        var newSignatureAccount = await signatureAccountService.UpdateTokens(signatureAccount);
        if (newSignatureAccount == null)
            throw apiEx;

        UpdateSignatureAccount = true;
        StorageService.SignatureAccount.SaveState(newSignatureAccount);
        return request.Invoke(newSignatureAccount, requestBody); 
    }
}

This works well enough since all the requests that'll be passing through this method have the same signature and all of them will return a Task.

My only issue with this code is that I need to call await 2 when I call it, for example:

var credentialsListResponse =  await await RunRequestWithRetry(GetCredentialsList, process.SignatureAccount, credentialsListBody);

I tried to return only the T_Out since I specified it to be of type Task but visual studio does not like that... So I had to wrap the return with a Task, but this makes my method return Task<Task<Type>>.

Is there a way to make this more elegant, maybe remove one of the await calls.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ BTW You can call the Unwrap, (1) to flatten Task<Task<T>> to Task<T> \$\endgroup\$ Dec 6 '21 at 8:14
  • \$\begingroup\$ A cool method to know, thanks! \$\endgroup\$ Dec 6 '21 at 8:27
3
\$\begingroup\$

Here are few suggestions :

First, make a loop

  • the readability is improved as you will see only request.Invoke(signatureAccount, requestBody) call in one place.
  • you will get rid of the nested Task
  • it will be more simple if your customer decide to ask 3 retries or more.

Second, RunRequestWithRetry is fullfilling too much roles. It should focus only on retrying a request as its name suggest. Refreshing tokens have no place here. A trick could be passing a "CleanupOnException" task as argument that the caller should provide.

Finally, if you can use some nuget packages, there is a very serious and popular one (top 7 non-microsoft nuget packages) which do exactly what you want in an elegant manner: Polly.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your sugestion, I dont really require do define a number of retries, I only need to retry once in case the token has expired, thats why the code to refresh the token is there, but I could it refresh the tokens in a separate method, anyways I managed to find a solution for the problem I was having. \$\endgroup\$ Dec 2 '21 at 15:57
0
\$\begingroup\$

I found out what was wrong with my logic, it was quite a simple fix that solves the double await problem, I just needed to make the signature of the method Func<SignatureAccount, T_In, Task<T_Out>> this way the T_Out will be the correct type and I can return Task<T_Out>.

I also took the advise by @Perfect28 and extracted the refreshToken logic to a new method called UpdateTokensAndRetry, this works for me since I only need to retry once after refreshing the tokens, since I only want to refresh when the tokens exipe.

Here's the code sample with the solution:

private async Task<T_Out> RunRequestWithRetry<T_In, T_Out>(Func<SignatureAccount, T_In, Task<T_Out>> request, SignatureAccount signatureAccount, T_In requestBody)
{
    try
    {
        return await request.Invoke(signatureAccount, requestBody);
    }
    catch (AMAApiException<ErrorResult> apiEx)
    {
        if (apiEx.ErrorType != AMAErrorType.TokenNeedsRefresh)
            throw apiEx;

        return await UpdateTokensAndRetry(request, signatureAccount, requestBody, apiEx);
    }
}

private async Task<T_Out> UpdateTokensAndRetry<T_In, T_Out, T_Ex>(Func<SignatureAccount, T_In, Task<T_Out>> request, SignatureAccount signatureAccount, T_In requestBody, T_Ex apiEx) where T_Ex : Exception
{
    // Refresh Tokens
    var signatureAccountService = new SignatureAccountService(MvcApplication.Config);
    var newSignatureAccount = await signatureAccountService.UpdateTokens(signatureAccount);
    if (newSignatureAccount == null)
        throw apiEx;

    UpdateSignatureAccount = true;
    StorageService.SignatureAccount.SaveState(newSignatureAccount);
    return await request.Invoke(newSignatureAccount, requestBody);
}

And now I can call the method like this:

var signHashesResponse = await RunRequestWithRetry(GetSignedHashes, process.SignatureAccount, signHashesRequest);
\$\endgroup\$
2
  • 1
    \$\begingroup\$ This answer is okay, but it might be better to delete it and ask a follow up question with a link pointing back to this question. \$\endgroup\$
    – pacmaninbw
    Dec 2 '21 at 16:17
  • \$\begingroup\$ I'm sorry, I dont quite understand what you are trying to tell me, what follow up question should I make? I don't get why I should delete it? \$\endgroup\$ Dec 2 '21 at 16:42

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.