Implementacja: PrintQueueExamples
Opis mechanizmu kolejki asynchronicznej
Klasa demonstracyjna PrintQueueExamples obrazuje model integracji dedykowany dla systemów klasy ERP oraz WMS realizujących operacje masowego generowania dokumentów. W przeciwieństwie do metod synchronicznych, które blokują wątek aplikacji klienckiej na czas renderowania pliku, model kolejkowy deleguje przetwarzanie raportu do wewnętrznego procesora tła opartego na architekturze Hangfire.
Kluczową zaletą tego podejścia jest natychmiastowe przyjęcie zlecenia przez sPrint API (odpowiedź w czasie kilku milisekund), co pozwala na asynchroniczne monitorowanie stanu procesu i bezpieczne pobranie gotowego dokumentu w optymalnym momencie.
Cykl życia zadania w kolejce (Asynchronous Workflow)
Cykl przetwarzania asynchronicznego zaimplementowany w klasie opiera się na trzech etapach komunikacji z API:
- 1. Inicjalizacja i Enkowanie (EnqueueAsync): Klient przesyła kompletny model żądania PrintViewModel na endpoint kolejki. Serwer weryfikuje strukturę, trwale zapisuje zadanie w bazie danych i natychmiast zwraca obiekt QueueResponseViewModel zawierający unikalny, techniczny identyfikator zadania JobId oraz biznesowy OperationId.
- 2. Monitorowanie Stanu (StatusAsync): Aplikacja kliencka uruchamia pętlę odpytywania (Polling) z bezpiecznym interwałem czasowym (instrukcja Task.Delay(2000)). Zadanie w tym czasie przechodzi przez stany systemowe: Enqueued, Processing, aż do osiągnięcia stanu końcowego: Succeeded lub Failed.
- 3. Konsumpcja Wyniku (ResultAsync): Po otrzymaniu statusu sukcesu, zachowanie aplikacji zależy od zadeklarowanego modelu wyjściowego (Output Model). Jeżeli użyto modelu ByteOutputViewModel, gotowy plik jest pobierany bezpośrednio z API jako strumień. W przypadku użycia PathOutputViewModel, serwer samodzielnie zapisuje dokument na wskazanym dysku sieciowym.
Pełny kod źródłowy przykładu (C#)
Poniższa implementacja w języku C# dla platformy .NET 8 prezentuje produkcyjne oprogramowanie pętli sprawdzania statusów, asynchroniczne kopiowanie strumieni binarnych (CopyToAsync) oraz konfigurację poświadczeń licencyjnych w obiekcie DataViewModel:
using Comarch.BI.CounterSprint.Integration.Client;
namespace Comarch.BI.CounterSprint;
/// <summary>
/// Contains examples of working with print queue functionality
/// </summary>
public static class PrintQueueExamples
{
private static readonly string ReportPath = Path.GetFullPath(@"Packages\JsonSource\CounterSprintJsonMain.sp");
private static readonly string ReportJsonSourcePath = Path.GetFullPath(@"Packages\JsonSource\products.json");
/// <summary>
/// Demonstrates printing a report with byte output
/// </summary>
public static async Task PrintQueueExample_ByteOutput(IntegrationApiClient client)
{
Console.WriteLine("Starting print job with byte output...");
var printRequest = await CreatePrintRequestFromBytes__ByteOutput();
Console.WriteLine($"Created print request with operation ID: {printRequest.OperationId}");
// Wysłanie zadania na endpoint kolejki asynchronicznej
var result = await PrintQueueExamples.SendPrintRequestToQueue(client, printRequest);
Console.WriteLine($"Print request enqueued. Job ID: {result.JobId}");
var status = "";
Console.WriteLine("Waiting for job completion...");
// Pętla Pollingu - sprawdzanie stanu zadania w tle
while (status != "Succeeded" && status != "Failed")
{
// Bezpieczny interwał czasowy przed kolejnym zapytaniem
await Task.Delay(2000);
// Pobranie aktualnego statusu z Hangfire za pomocą JobId
var statusResponse = await PrintQueueExamples.GetPrintJobStatus(client, result.JobId);
status = statusResponse.Status;
Console.WriteLine($"Current job status: {status}");
}
if (status == "Succeeded")
{
Console.WriteLine("Print job succeeded. Retrieving results...");
// Pobranie pliku z bazy pamięciowej API za pomocą biznesowego OperationId
var fileBytes = await PrintQueueExamples.GetResult(client, result.OperationId);
using (var memoryStream = new MemoryStream())
{
await fileBytes.Stream.CopyToAsync(memoryStream);
await File.WriteAllBytesAsync("PrintQueueResult.pdf", memoryStream.ToArray());
}
Console.WriteLine("Print queue result saved to PrintQueueResult.pdf");
}
else
{
Console.WriteLine("Print job failed. Please check logs for more details.");
}
}
/// <summary>
/// Demonstrates printing a report with path output
/// </summary>
public static async Task PrintQueueExample_PathOutput(IntegrationApiClient client)
{
Console.WriteLine("Starting print job with path output...");
var printRequest = await CreatePrintRequestFromBytes__PathOutput();
Console.WriteLine($"Created print request with operation ID: {printRequest.OperationId}");
Console.WriteLine($"Output will be saved to: {(printRequest.Output as PathOutputViewModel)?.Path}");
var result = await PrintQueueExamples.SendPrintRequestToQueue(client, printRequest);
Console.WriteLine($"Print request enqueued. Job ID: {result.JobId}");
var status = "";
Console.WriteLine("Waiting for job completion...");
while (status != "Succeeded" && status != "Failed")
{
await Task.Delay(2000);
var statusResponse = await PrintQueueExamples.GetPrintJobStatus(client, result.JobId);
status = statusResponse.Status;
Console.WriteLine($"Current job status: {status}");
}
if (status == "Succeeded")
{
Console.WriteLine("Print job succeeded, file saved to defined path in PathOutputViewModel");
}
else
{
Console.WriteLine("Print job failed. Please check logs for more details.");
}
}
private static async Task<QueueResponseViewModel> SendPrintRequestToQueue(IntegrationApiClient client, PrintViewModel printRequest)
{
Console.WriteLine("Sending print request to queue...");
return await client.EnqueueAsync(printRequest);
}
private static async Task<QueueStatusResponseViewModel> GetPrintJobStatus(IntegrationApiClient client, string jobId)
{
Console.WriteLine($"Checking status for job ID: {jobId}");
return await client.StatusAsync(jobId);
}
private static async Task<FileResponse> GetResult(IntegrationApiClient client, string operationId)
{
Console.WriteLine($"Retrieving results for operation ID: {operationId}");
return await client.ResultAsync(operationId);
}
private static async Task<PrintViewModel> CreatePrintRequestFromBytes__ByteOutput()
{
return new ()
{
OperationId = Guid.NewGuid().ToString(),
Report = new ByteSourceViewModel { Source = await File.ReadAllBytesAsync(ReportPath) },
PrintFormat = FileFormat.Pdf,
Output = new ByteOutputViewModel(),
DataSources = [new JsonDataSourceViewModel
{
JsonSource = new ByteSourceViewModel { Source = await File.ReadAllBytesAsync(ReportJsonSourcePath) }
}],
Parameters = [new ParameterToReplaceViewModel
{
Name = "group_name",
Value = "Furniture"
}],
Data = new DataViewModel()
{
Server = "localhost",
Key = "5000311484",
Port = 5150,
Type = DataType.Sql
}
};
}
private static async Task<PrintViewModel> CreatePrintRequestFromBytes__PathOutput()
{
var outputPath = Path.GetFullPath(@"PrintQueueResult_PathOutput.pdf");
return new ()
{
OperationId = Guid.NewGuid().ToString(),
Report = new ByteSourceViewModel { Source = await File.ReadAllBytesAsync(ReportPath) },
PrintFormat = FileFormat.Pdf,
Output = new PathOutputViewModel { Path = outputPath },
DataSources = [new JsonDataSourceViewModel
{
JsonSource = new ByteSourceViewModel { Source = await File.ReadAllBytesAsync(ReportJsonSourcePath) }
}],
Parameters = [new ParameterToReplaceViewModel
{
Name = "group_name",
Value = "Furniture"
}],
Data = new DataViewModel()
{
Server = "localhost",
Key = "5000311484",
Port = 5150,
Type = DataType.Sql
}
};
}
}
Kluczowe aspekty architektoniczne (Różnica JobId vs OperationId)
Najbardziej krytycznym punktem integracji z modułem kolejkowym sPrint API jest bezbłędne zrozumienie przeznaczenia dwóch odrębnych identyfikatorów zwracanych przez system:
- JobId (Identyfikator techniczny): Jest to unikalny token generowany przez system kolejkowania Hangfire. Służy on wyłącznie do sprawdzania bieżącego stanu procesu przetwarzania w tle na endpoincie statusowym (metoda client.StatusAsync(jobId)). Identyfikator ten nie jest powiązany z fizycznym plikiem wynikowym.
- OperationId (Identyfikator biznesowy): Jest to unikalny klucz przesyłany przez Twoją aplikację lub generowany dla transakcji. Służy on wyłącznie do autoryzacji i pobrania wygenerowanego dokumentu binarnego z repozytorium pamięciowego sPrint (metoda client.ResultAsync(operationId)).