// ============================================================================= // GB5 Framework — ReportExecutionResult // Namespace : GB5Shared.DTO.ReportOrchestration // Purpose : Unified result returned by IQueryOrchestrator.ExecuteAsync(). // Covers sync (data/stream ready), async (SysJob submitted), // and error outcomes. The caller (API endpoint / Angular client) // inspects ResultType to determine how to respond to the browser. // ============================================================================= using GB5Shared.Enums.ReportOrchestration; namespace GB5Shared.DTO.ReportOrchestration { // ------------------------------------------------------------------------- // ResultType — discriminates the three possible outcomes // ------------------------------------------------------------------------- /// /// Discriminates the three possible outcomes of an orchestrated report call. /// public enum ReportResultType : byte { /// /// Report executed synchronously. Data or export stream is ready. /// Caller should stream the result to the browser immediately. /// SyncCompleted = 0, /// /// Report submitted as SysJob. Execution is pending. /// Caller should return SysJobId to the Angular client. /// Angular shows "My Reports" inbox; SignalR delivers completion. /// AsyncSubmitted = 1, /// /// Execution failed. Error details are in ErrorMessage / Exception. /// Caller should return an appropriate error response. /// Failed = 2 } // ------------------------------------------------------------------------- // ReportExecutionResult // ------------------------------------------------------------------------- /// /// Unified result returned by . /// Inspect before accessing other properties. /// public sealed class ReportExecutionResult { // --------------------------------------------------------------------- // DISCRIMINATOR // --------------------------------------------------------------------- /// Outcome of the orchestrated execution. public ReportResultType ResultType { get; private init; } // --------------------------------------------------------------------- // SYNC RESULT FIELDS (valid when ResultType = SyncCompleted) // --------------------------------------------------------------------- /// /// The export format of the produced output. /// Valid when ResultType = SyncCompleted. /// public ExportFormat ExportFormat { get; private init; } /// /// Flat data rows for Grid format, or raw data before export processing. /// Each row is a dictionary of fieldName → value matching MREPORTVSFIELDS. /// Valid when ResultType = SyncCompleted AND ExportFormat = Grid. /// Null for binary export formats (PDF, Excel, CSV) — use ExportStream. /// public IReadOnlyList>? GridData { get; private init; } /// /// Total row count before any paging. /// Valid when ResultType = SyncCompleted AND ExportFormat = Grid. /// public int TotalRows { get; private init; } /// /// Binary export stream for PDF / Excel / CSV. /// Valid when ResultType = SyncCompleted AND ExportFormat ≠ Grid. /// Caller must dispose after streaming to response. /// Null for Grid format — use GridData. /// public Stream? ExportStream { get; private init; } /// /// MIME type for the export stream. /// e.g. "application/pdf", "application/vnd.openxmlformats-..." /// Valid when ExportStream is not null. /// public string? ContentType { get; private init; } /// /// Suggested file name for browser download (without path). /// e.g. "SalesAnalysis_2025-01.xlsx" /// Valid when ExportStream is not null. /// public string? FileName { get; private init; } /// /// True when the result was served from cache (not re-executed). /// Informational only — behaviour is identical to a fresh result. /// public bool IsFromCache { get; private init; } // --------------------------------------------------------------------- // ASYNC RESULT FIELDS (valid when ResultType = AsyncSubmitted) // --------------------------------------------------------------------- /// /// The TSYSJOB.SYSJOBID submitted by the orchestrator. /// Valid when ResultType = AsyncSubmitted. /// Return this to the Angular client so it can track progress /// via SignalR or "My Reports" polling. /// public int SysJobId { get; private init; } = -1; /// /// Human-readable message to display to the user while the job runs. /// Valid when ResultType = AsyncSubmitted. /// e.g. "Your report is being generated. You will be notified when ready." /// public string? AsyncMessage { get; private init; } // --------------------------------------------------------------------- // ERROR FIELDS (valid when ResultType = Failed) // --------------------------------------------------------------------- /// /// User-friendly error description. /// Valid when ResultType = Failed. /// Never expose raw exception messages to the client. /// public string? ErrorMessage { get; private init; } /// /// Error code for structured error handling by the Angular client. /// e.g. "DATASOURCE_UNAVAILABLE", "TIMEOUT", "ACCESS_DENIED". /// Valid when ResultType = Failed. /// public string? ErrorCode { get; private init; } /// /// The underlying exception — available server-side for logging. /// Never serialise to the client response. /// Valid when ResultType = Failed. /// public Exception? Exception { get; private init; } // --------------------------------------------------------------------- // DIAGNOSTICS — always populated // --------------------------------------------------------------------- /// /// Correlation ID from . /// Include in all error responses so support can correlate server logs. /// public Guid CorrelationId { get; private init; } /// /// UTC timestamp when execution completed (or was submitted for async). /// public DateTime CompletedAtUtc { get; private init; } = DateTime.UtcNow; /// /// Wall-clock time taken from context creation to result. /// Includes DB resolution, query, and export. /// public TimeSpan ElapsedTime { get; private init; } /// /// The DataSourceType that was actually used for this execution. /// Useful for the Angular client to display "Data from: Archive DB". /// public DataSourceType DataSourceUsed { get; private init; } // --------------------------------------------------------------------- // FACTORY METHODS — the only way to construct results // Use these in the orchestrator implementation; no public constructor. // --------------------------------------------------------------------- /// /// Creates a successful sync result carrying flat Grid data. /// public static ReportExecutionResult SyncGrid( IReadOnlyList> data, int totalRows, DataSourceType dataSourceUsed, Guid correlationId, TimeSpan elapsed, bool fromCache = false) => new() { ResultType = ReportResultType.SyncCompleted, ExportFormat = ExportFormat.Grid, GridData = data, TotalRows = totalRows, IsFromCache = fromCache, DataSourceUsed = dataSourceUsed, CorrelationId = correlationId, ElapsedTime = elapsed }; /// /// Creates a successful sync result carrying a binary export stream. /// public static ReportExecutionResult SyncExport( Stream stream, string contentType, string fileName, ExportFormat format, DataSourceType dataSourceUsed, Guid correlationId, TimeSpan elapsed) => new() { ResultType = ReportResultType.SyncCompleted, ExportFormat = format, ExportStream = stream, ContentType = contentType, FileName = fileName, DataSourceUsed = dataSourceUsed, CorrelationId = correlationId, ElapsedTime = elapsed }; /// /// Creates an async submission result (SysJob queued successfully). /// public static ReportExecutionResult AsyncQueued( int sysJobId, DataSourceType dataSourceUsed, Guid correlationId, TimeSpan elapsed, string? message = null) => new() { ResultType = ReportResultType.AsyncSubmitted, SysJobId = sysJobId, AsyncMessage = message ?? "Your report is being generated. You will be notified when it is ready.", DataSourceUsed = dataSourceUsed, CorrelationId = correlationId, ElapsedTime = elapsed }; /// /// Creates a failure result. Exception is retained for server-side /// logging; never expose it to the client directly. /// public static ReportExecutionResult Failure( string errorMessage, string errorCode, Guid correlationId, TimeSpan elapsed, Exception? ex = null) => new() { ResultType = ReportResultType.Failed, ErrorMessage = errorMessage, ErrorCode = errorCode, Exception = ex, CorrelationId = correlationId, ElapsedTime = elapsed }; } }