przesyłanie zamówień do zewn systemu - implementacja BackgroundService

przesyłanie zamówień do zewn systemu - implementacja BackgroundService
JD
  • Rejestracja:około 19 lat
  • Ostatnio:3 dni
0

Hej, chciałbym prosić Was o opinie. Proces jest taki: user składa zamówienie, złożone zamówienie musi lecieć do zewn systemu przy pomocy restowego API.
Mechanizm działa, natomiast pytanie czy to jest OK? czy coś byście zmienili, usprawnili, poprawili?

kontroler

Kopiuj
public class OrderController(IOrderService orderService, IOrderProcessingService orderProcessingService) : ControllerBase
{
    [HttpPost("add")]
    public async Task<IActionResult> RegisterOrder([FromBody] OrderRequest orderRequest)
    {
        if(orderRequest == null)
        {
            return BadRequest();
        }
        
        var order = await orderService.CreateOrder(orderRequest);

        if(order is null)
        {
            return StatusCode(500, "Order creation failed");
        }

        //Prepare object for sending
        var orderToErp = orderService.PrepareOrder(order);

        if (orderToErp == null)
        {
            return StatusCode(500, "Failed to prepare order for ERP.");
        }

        // Enqueue the order for background processing and send to ERP
        orderProcessingService.EnqueueOrder(orderToErp);
// ... reszta kodu
}
Kopiuj
public class OrderQueue
{
    private readonly ConcurrentQueue<ERP_OrderRootObject> _orders = new ConcurrentQueue<ERP_OrderRootObject>();

    public void Enqueue(ERP_OrderRootObject orderToErp) {  _orders.Enqueue(orderToErp); }

    public bool TryDequeue(out ERP_OrderRootObject orderToErp)
    {
        return _orders.TryDequeue(out orderToErp);
    }
}
Kopiuj
public interface IOrderProcessingService
{
    Task SendOrderToErpAsync(ERP_OrderRootObject orderToErp);
    void EnqueueOrder(ERP_OrderRootObject orderToErp);
}

public class OrderProcessingService(
    OrderQueue orderQueue, 
    ILogger<OrderProcessingService> logger,
    IServiceScopeFactory serviceScopeFactory
) : BackgroundService, IOrderProcessingService
{
    public void EnqueueOrder(ERP_OrderRootObject orderToErp)
    {
        orderQueue.Enqueue(orderToErp);
    }

    public async Task SendOrderToErpAsync(ERP_OrderRootObject orderToErp)
    {
        // Resolve the scoped service within the method
        using (var scope = serviceScopeFactory.CreateScope())
        {
            logger.LogInformation("....:::: BEGIN : sending something to ERP in the background ::::....");

            var microsoftService = scope.ServiceProvider.GetRequiredService<IMicrosoftService>();
            var httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();

            // generalnie komunikacja z zewn. API 

            logger.LogInformation("....:::: END : sending something to D365 in the background ::::....");
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (orderQueue.TryDequeue(out var orderToErp))
            {
                try
                {
                    await SendOrderToErpAsync(orderToErp);
                    logger.LogInformation("Order {OrderId} sent to ERP successfully.", orderToErp.OrderHeaderClass.MessageId);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Failed to send order {OrderId} to ERP.", orderToErp.OrderHeaderClass.MessageId);
                    // Handle failure (e.g., retry, log for manual processing, etc.)
                }
            }
            await Task.Delay(1000, stoppingToken);
        }
    }
}

oraz rejestracja

Kopiuj
        // Register the order queue as a singleton
        services.AddSingleton<OrderQueue>();

        // Register the factory and background service
        services.AddSingleton<IOrderProcessingService, OrderProcessingService>();
        services.AddHostedService<OrderProcessingService>();
edytowany 1x, ostatnio: john_doe
SL
  • Rejestracja:około 7 lat
  • Ostatnio:około 4 godziny
  • Postów:904
1

Dobrze jest mieć persystentą kolejkę, która nie traci wiadomości jak twoja instancja się zwija lub zabraknie prądu z gniazdku. Z drugiej strony to dużo roboty. O detalach typowych dla C# się nie wypowiem, ale wygląda dobrze

orderService.PrepareOrder(order);

Co jak to zwróci null? Będziesz miał utworzony order, ale event się nie wygeneruje

JD
  • Rejestracja:około 19 lat
  • Ostatnio:3 dni
0

orderService.PrepareOrder(order);

Co jak to zwróci null? Będziesz miał utworzony order, ale event się nie wygeneruje

jeszcze nie wiem. PrepareOrder to tak naprawdę manualne mapowanie założonego ordera na order akceptowalny przez zewn API
To dość ważne bo jakby zwrócił null nie wykona się reszta kodu a order jest zapisany. Z drugiej strony jeśli order zostanie założony to mapowanie powinno przejść również.
Ale to bardzo dobre pytanie ...

WeiXiao
  • Rejestracja:około 9 lat
  • Ostatnio:około 2 godziny
  • Postów:5146
1

Zamiast trzymac to w w pamięci to polecam zapisywac np. do bazki, a dopiero później przetwarzać

Robiąc coś podobnego bardzo ułatwiło mi zrobienie maszyny stanów na to np. w Order masz proprety State i jakiś enum np. New, SentToMicrosoft, SentToBlaBla, ProcessCompleted

Później wyciagles sobie wszystkie ordery w stanie New i próbowałeś na nich zrobić #1 krok i jezeli się udało, to updatowałeś na stan po 1 kroku (SentToMicrosoft)

edytowany 1x, ostatnio: WeiXiao
markone_dev
  • Rejestracja:ponad 3 lata
  • Ostatnio:2 dni
  • Postów:822
3

Zrób Outbox Pattern. W skrócie zapisujesz event/wiadomość w bazie ze statusem New i osobny proces/aplikacja w tle co jakiś czas zczytuje sobie eventy z tej tabeli które mają status New i je przetwarza (w twoim przypadku wysyła do zewnętrznego systemu). Jak operacja się powiedzie to aktualizuje status na Processed, a jak się nie powiedzie to na Error wraz z informacją o błędzie, godzinie wystąpienia itd.

Dzięki temu nie ma problemu, że jak aplikacja się wysypie to wszystkie zdarzenia/wiadomości siedzące w pamięci znikną i przetwarzając je możesz robić ponowienia (retry pattern) w przypadku gdy nie uda się wysłać wiadomości, proces wysyłający do zewnętrznego systemu jest niezależny od tego czy aplikacja działa czy nie. Nawet jak się aplikacja wyłoży to informacje które zostały oznaczone w bazie jako do przetworzenia to zostaną przetworzone.

A gdybyś potrzebował integracji w czasie rzeczywistym to na ratunek przychodzi CDC (change data capture), ale to już wyższa szkoła jazdy i założę się że spokojnie możesz przetwarzać te wiadomości w batchach co 15-20 minut,


Programujący korpo architekt chmurowy.
Udzielam konsultacji i szkoleń w obszarze szeroko pojętego cloud computingu (Azure, AWS) i architektury systemów IT. Dla firm i prywatnie.
DevOps to proces nie stanowisko.
SZ
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 3 godziny
  • Postów:1514
0
markone_dev napisał(a):

Zrób Outbox Pattern. W skrócie zapisujesz event/wiadomość w bazie ze statusem New i osobny proces/aplikacja w tle co jakiś czas zczytuje sobie eventy z tej tabeli które mają status New i je przetwarza

Zrobiłbym małą poprawkę do tego. Zapis eventu w bazie. Potem wysłanie na kolejkę in memory do przetworzenia. A background service odczytuje eventy do przetworzenia tylko przy starcie aplikacji. Dzięki temu unikamy cyklicznego odpytywania bazy.

KL
Jak na kolejkę in memory?

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.