I wrote this small service which emulates a payment registration system.
My main concern is thread-safety in general, and the caller shouldn't be able to register a payment while one is already in progress.
The service code:
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
namespace OpenPaymentApi.Services;
public class ThreadSafeCollectionsPaymentService : IPaymentService
{
private readonly ConcurrentBag<RegisteredPayment> _payments = new();
private readonly ConcurrentDictionary<string, object> _paymentRequestsInProgress = new();
private readonly ServerSettingsOptions _serverSettings;
public ThreadSafeCollectionsPaymentService(IOptions<ServerSettingsOptions> serverSettingsOption)
{
_serverSettings = serverSettingsOption.Value;
}
public async Task<Result<string>> RegisterPayment(string clientId, PaymentRequest paymentRequest)
{
var paymentInProgress = _paymentRequestsInProgress.ContainsKey(clientId);
if (paymentInProgress)
{
return Result<string>.Error("Payment already in progress for the client");
}
_paymentRequestsInProgress[clientId] = new object();
Thread.Sleep(TimeSpan.FromSeconds(_serverSettings.PaymentProcessingTimeInSeconds));
var payment = new RegisteredPayment
{
Currency = paymentRequest.Currency,
Creditor_Account = paymentRequest.Creditor_Account,
Debtor_Account = paymentRequest.Debtor_Account,
Transaction_Amount = paymentRequest.Instructed_Amount,
Payment_ID = Guid.NewGuid().ToString(),
};
_payments.Add(payment);
_paymentRequestsInProgress.Remove(clientId, out _);
return Result<string>.Success(payment.Payment_ID);
}
public async Task<Result<RegisteredPayment[]>> GetPayments(string iban)
{
var result = _payments.Where(p => p.Creditor_Account == iban || p.Debtor_Account == iban).ToArray();
return Result<RegisteredPayment[]>.Success(result);
}
}
And I registered it as a singleton in program.cs like this:
builder.Services.AddSingleton<IPaymentService,ThreadSafeCollectionsPaymentService>();
and the service is being called from the controller like this:
[HttpPost]
[Route("/payments")]
public async Task<IActionResult> PostPayment([FromHeader(Name = "Client-ID")] string clientId,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Disallow)] PaymentRequest paymentRequest)
{
if (string.IsNullOrWhiteSpace(clientId))
return BadRequest();
var paymentResult = await _paymentService.RegisterPayment(clientId, paymentRequest);
if (paymentResult.IsSuccess)
{
return Ok(paymentResult.Data);
}
return Conflict(paymentResult.ErrorMessage);
}