In my current solution, a user triggers my web API from a web application of mine first, then my web API calls a 3rd party web service, and the results/codes are displayed on the web application. This current design (web application <-> web API <-> 3rd party web service) forces the user to stay in front of the screen and keep constantly sending requests to the API until successfully fulfilling 20K codes. This 3rd party web API returns a maximum of 10 results/codes inside of a single request. There are times it is needed to get 20.000 results/codes from this service.
The user has to stay in front of the screen for hours to get 20000 codes. This can be tedious work, requiring a great deal of dedication and focus. The aim of my project is to reduce the dependency on the user and reduce the total time spent with the service in the background. To achieve this, my project will automate the process of generating the codes, thus reducing the user's workload and freeing them up to focus on other tasks.
I implemented an IHostedService that calls this 3rd party web API in the background. I need your feedback in terms of speed, error handling and etc. How can I improve my solution? Do you have any comments?
Here is the IHostedService:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
public Worker(ILogger<Worker> logger, IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
//while (!stoppingToken.IsCancellationRequested)
//{
// await GetData();
// _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
// /*await Task.Delay(1000, stoppingToken);*/
//}
//Timer with 3 mins
var timer = new PeriodicTimer(TimeSpan.FromMinutes(3));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await GetData();
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
}
private async Task MakeRequestsToRemoteService(string productCode, long id)
{
if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id));
try
{
var httpClient = _httpClientFactory.CreateClient("RazerClient");
//Token
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
var tokenContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("username", "Test"),
new KeyValuePair<string, string>("password", "123456")
});
using var tokenResponse = await httpClient.PostAsync(_configuration["Token:Production"], tokenContent);
if ((int)tokenResponse.StatusCode == 200)
{
var token = await tokenResponse.Content.ReadAsStringAsync();
//Call Razer Multi Requests
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
token);
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
//without Parallel.ForEach
//var content = new FormUrlEncodedContent(new[]
//{
// new KeyValuePair<string, string>("productCode", productCode),
// new KeyValuePair<string, string>("quantity", "100")
//});
////Parallel.ForEachAsync
//using var response = await httpClient.PostAsync(_configuration["Razer:Production"], content);
//if ((int)response.StatusCode == 200)
//{
// var coupon = await response.Content.ReadFromJsonAsync<Root>();
// _logger.LogInformation("Order {referenceId} {pin}, {Serial} ", coupon.ReferenceId,
// coupon.Coupons[0].Pin, coupon.Coupons[0].Serial);
// await UpdateData(id);
//}
//else
//{
// _logger.LogError("Purchase ServiceError: {statusCode}",
// (int)response.StatusCode);
//}
var quantities = new List<int> { 100, 100, 100, 100, 100 };
var cts = new CancellationTokenSource();
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = 2,
CancellationToken = cts.Token
};
try
{
await Parallel.ForEachAsync(quantities, parallelOptions, async (quantity, ct) =>
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("productCode", productCode),
new KeyValuePair<string, string>("quantity", quantity.ToString())
});
using var response = await httpClient.PostAsync(_configuration["Razer:Production"], content, ct);
if ((int)response.StatusCode == 200)
{
var coupon = await response.Content.ReadFromJsonAsync<Root>(cancellationToken: ct);
_logger.LogInformation("REFERENCE ID: {referenceId}", coupon.ReferenceId);
await UpdateData(id);
}
else
{
_logger.LogError("Purchase ServiceError: {statusCode}",
(int)response.StatusCode);
}
});
}
catch (OperationCanceledException ex)
{
_logger.LogError("Operation canceled: {Message}",
ex.Message);
}
}
else
{
_logger.LogError("Token ServiceError: {statusCode}",
(int)tokenResponse.StatusCode);
}
}
catch (Exception e)
{
_logger.LogError("Error: {Error} ", e.Message);
throw;
}
}
private async Task GetData()
{
var sw = Stopwatch.StartNew();
var connString = _configuration["ConnectionStrings:Default"];
await using var sqlConnection = new SqlConnection(connString);
sqlConnection.Open();
await using var command = new SqlCommand { Connection = sqlConnection };
const string sql = @"Select TOP 1 Id, BulkId, Amount, ProductCode from BulkPurchases where status = 0";
command.CommandText = sql;
try
{
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
_logger.LogInformation(
"Order {Id}, {BulkId}, {Amount}, {ProductCode}",
reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetString(3));
await MakeRequestsToRemoteService(reader.GetString(3).Trim(), reader.GetInt32(0));
}
}
catch (SqlException exception)
{
_logger.LogError("Error: {Error} ", exception.Message);
throw; // Throw exception if this exception is unexpected
}
sw.Stop();
_logger.LogInformation($"******** ELAPSED TIME: {sw.Elapsed.TotalSeconds} seconds ********");
}
private async Task UpdateData(long id)
{
var connString = _configuration["ConnectionStrings:Default"];
await using var sqlConnection = new SqlConnection(connString);
sqlConnection.Open();
await using var command = new SqlCommand { Connection = sqlConnection };
const string sql = @"Update BulkPurchases set status = 1 where Id = @id";
command.CommandText = sql;
command.Parameters.Add(new SqlParameter("id", id));
try
{
await command.ExecuteNonQueryAsync();
}
catch (SqlException exception)
{
_logger.LogError("Error: {Error} ", exception.Message);
throw; // Throw exception if this exception is unexpected
}
}
}