I'm writing one Webapi where I've to call a 2nd endpoint of HTTP when the first HTTP endpoint fails. However, for the second HTTP call, my application shouldn't block the main thread and should return an unsuccessful result. My HTTP client is already equipped with Polly retry policies, and after retries, I want to call the 2nd URL silently without blocking the main thread. My code looks like below.
public class Request
{
public string Id { get; set; }
}
public class Result
{
public string Id { get; set; }
public bool IsSuccess { get; set; }
public string Message { get; set; }
}
public class Service
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly Ilogger _appLogger;
private readonly string _url1="www.somedomain.com/api1";
private readonly string _url2="www.somedomain.com/api2";
Service(IHttpClientFactory factory,Ilogger logger)
{
_httpClientFactory = httpClientFactory;
_appLogger = logger;
}
public async Task<Result> Send(Request request)
{
try
{
using var client = _httpClientFactory.CreateClient();
using HttpRequestMessage httpRequest =new HttpRequestMessage(HttpMethod.Post, _url1);
var req = JsonConvert.SerializeObject<Request>(body);
var stringContent = new StringContent(req, Encoding.UTF8, "json");
var result = await client.SendAsync(httpRequest);
var resultContent = await result.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Result>(resultContent));
}
catch (Exception ex)
{
if (ex is OperationCanceledException || ex is TaskCanceledException)
{
//Second call needed but don't want to hold the result
// Task.Run(async ()=> await Log(request)) //Approach 1
// Task.Factory.StartNew(async () => await Log(request)) //Approach 2
// HostingEnvironment.QueueBackgroundWorkItem(ct => Log(request).ConfigureAwait(false)); //Approach 3
// QueuedHostedService defined here [Microsoft][1]
// var jobId = BackgroundJob.Enqueue(() => Log(request).ConfigureAwait(false)); //Approach 4 by hangfire [Hangfire][2]
// Custome singleton suggested here // [CustomImplementation][1]
}
//No 2nd call needed
await _appLogger.LogAsync(new ExceptionLog(ex));
return Result{IsSuccess=false;};
}
}
public async Task Log(Request request)
{
try
{
using var client = _httpClientFactory.CreateClient();
using var httpRequest =new HttpRequestMessage(HttpMethod.Post, _url2);
var req = JsonConvert.SerializeObject<Request>(body);
var stringContent = new StringContent(req, Encoding.UTF8, "json");
var result = await client.SendAsync(httpRequest);
var resultContent = await result.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(resultContent));
await _appLogger.LogAsync("2nd call successful.",result );
}
catch (Exception ex)
{
await _appLogger.LogAsync("2nd call failed",ex);
}
}
}
I was looking for many solutions but am now confused between them. [1]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio [2]: https://www.hangfire.io/ [3]: https://anduin.aiursoft.com/post/2020/10/14/fire-and-forget-in-aspnet-core-with-dependency-alive
CodePudding user response:
Actually, what you're looking for is a fire and forget
mechanism, that should work independently with current scope.
We all know that as soon as HttpRequest
finish their response, current scope including all the service that we use, would be dispose(default scope level behavior). So, let imagine 2nd call came back later, after the origin http request finish, everything would already disposed and clear out. Nasty error...
Take a look at this, and process it in your catch block and things should be good.
Another approach is make use of Background service, the idea is creating a queue that contain necessary information to let the background scan and process them, let say for whatever interval rate we want.
CodePudding user response:
I'll try to provide a breakdown of your possible approaches:
- Fire and forget call: It does work, is the easiest to implement, but it doesn't provide any guarantee or visibility that the code executed. Any exception inside a fire and forget method will be swallowed. If your application stops during the execution of a fire and forget method, you can't get any retry attempt. Use this approach if the call is not mandatory.
- Hangfire: It does what you want, it has several options like defining retry policies and setting how many parallel executions you can have. It supports several different storages, including in memory, SQL and NoSQL. Personally, I'm a big fan of HangFire and think it is the most robust approach while being easy to implement. However, it does add a dependency that you might have to discuss with the rest of your team.
- Hosted Service: I doesn't do what you want out of the box, as a Hosted Service is more like an app within an app. You can use it to build a custom persistent job execution, but you'll be pretty much reinventing a subset of Hangfire features. It is by far the most complex approach, and I'd avoid it if possible.