Skip to content

Report Generation ​

GrydReports supports three generation modes: synchronous (on-demand), asynchronous (background), and batch (multiple reports). All modes follow the same pipeline: validate → fetch data → render → store → deliver.

Generation Pipeline ​

Request → Validate Parameters → Fetch Data → Render → Store File → Deliver (optional)
              │                     │            │          │            │
          IReportFilter      IReportDataSource  IReportRenderer  IServerFileStore  IReportDelivery
         (OnBefore...)        (FetchDataAsync)  (RenderAsync)   (GrydFiles)       (DeliverAsync)

Generated files are stored in GrydFiles, in process, under the gryd.report profile. Each file is registered on behalf of its execution: when the file module's scan releases it, the execution holds a reference with owner scope gryd.report-execution and owner key equal to the execution id. The profile waives the antivirus (the file never left this process), but the server still computes the SHA-256 and checks the content against the declared type, so a renderer that produces something other than its format is rejected. Retention is UntilReleased: deleting the execution releases the reference, and the file is purged by the retention job after the grace period.

Breaking change — download returns a URL (GrydFiles)

GET /api/v1/reports/{id}/download used to stream the file. It now answers 200 with { "url": "…", "expiresAt": "…" }, a presigned URL valid for a few minutes, and the file is fetched from that URL, straight from storage. There is no route, flag or wrapper that still serves the bytes. Callers adapt in two steps: request the URL, then GET it (or redirect the browser to it). A report whose file is still being verified answers 409 FILE_NOT_AVAILABLE_CONFLICT; ask again a moment later. ReportResultDto.downloadUrl and ReportExecutionDto.fileUri were replaced by storedFileId, and IReportGenerator.GenerateAndStoreAsync/GenerateAndDeliverAsync now take the persisted ReportExecution instead of a template id and format. Ships in the next major version.

Synchronous Generation ​

Returns the generated file immediately. Best for small-to-medium reports:

Via API ​

bash
POST /api/v1/reports/generate
Content-Type: application/json

{
  "templateId": "monthly-sales",
  "format": 1,
  "parameters": {
    "month": 1,
    "year": 2026
  },
  "requestedBy": "user@company.com"
}

Response 201 Created:

json
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "templateId": "monthly-sales",
  "reportName": "Sales Report - January 2026",
  "format": 1,
  "status": 3,
  "fileName": "monthly-sales-2026-01.pdf",
  "fileSizeBytes": 245760,
  "storedFileId": "0b8e2f51-7c3d-4e6a-9f10-2a4b6c8d0e12",
  "generationDuration": "00:00:02.341",
  "createdAt": "2026-02-19T10:30:00Z"
}

Via MediatR ​

csharp
var result = await mediator.Send(new GenerateReportCommand(
    TemplateId: "monthly-sales",
    Format: ReportFormat.Pdf,
    Parameters: new Dictionary<string, object?>
    {
        ["month"] = 1,
        ["year"] = 2026
    },
    RequestedBy: "user@company.com"
));

if (result.IsSuccess)
{
    var report = result.Data!;
    Console.WriteLine($"Generated: {report.FileName} ({report.FileSizeBytes} bytes)");
}
else
{
    Console.WriteLine($"Error: {result.ErrorMessage}");
}

Asynchronous Generation ​

Queues the report for background processing. Returns immediately with a tracking ID. Ideal for large reports or when delivery is configured:

Via API ​

bash
POST /api/v1/reports/generate-async
Content-Type: application/json

{
  "templateId": "annual-financial",
  "format": 2,
  "parameters": {
    "year": 2025
  },
  "delivery": {
    "method": 2,
    "recipients": ["cfo@company.com", "finance@company.com"],
    "emailSubject": "Annual Financial Report 2025",
    "emailBody": "Please find the attached annual financial report."
  },
  "requestedBy": "admin@company.com"
}

Response 202 Accepted:

json
{
  "id": "a1b2c3d4-...",
  "templateId": "annual-financial",
  "status": 1,
  "createdAt": "2026-02-19T10:35:00Z"
}

Via MediatR ​

csharp
var result = await mediator.Send(new GenerateReportAsyncCommand(
    TemplateId: "annual-financial",
    Format: ReportFormat.Excel,
    Parameters: new Dictionary<string, object?> { ["year"] = 2025 },
    Delivery: new DeliveryOptions
    {
        Method = DeliveryMethod.Email,
        Recipients = ["cfo@company.com"]
    },
    RequestedBy: "admin@company.com"
));

// Returns immediately — report is generated in background
var trackingId = result.Data!.Id;

Polling for Status ​

bash
GET /api/v1/reports/{trackingId}

Status transitions: Queued (1) → Generating (2) → Completed (3) or Failed (4) → Delivered (5)

IReportGenerator ​

The central orchestrator that wires everything together:

csharp
public interface IReportGenerator
{
    // Generate and return the output stream
    Task<Result<ReportOutput>> GenerateAsync<TData, TParams>(
        string templateId, TParams parameters, ReportFormat format,
        CancellationToken ct = default);

    // Generate + store in GrydFiles on behalf of a persisted execution
    Task<Result<ReportResultDto>> GenerateAndStoreAsync<TData, TParams>(
        ReportExecution execution, TParams parameters,
        CancellationToken ct = default);

    // Generate + store + deliver the rendered bytes via the configured channel
    Task<Result<ReportResultDto>> GenerateAndDeliverAsync<TData, TParams>(
        ReportExecution execution, TParams parameters,
        DeliveryOptions delivery, CancellationToken ct = default);

    // List all registered report templates
    IReadOnlyList<ReportDefinitionDto> GetAvailableReports();
}

The execution must already be persisted: its tenant and id are what the file is stored under and on whose behalf. Scheduled runs create an execution for the schedule's tenant and go through the same path.

Download ​

Request a short-lived URL to a previously generated report:

bash
GET /api/v1/reports/{id}/download

Response 200 OK:

json
{
  "url": "https://storage.example.com/…?X-Amz-Signature=…",
  "expiresAt": "2026-09-10T18:05:00Z"
}

Then fetch the file from url. The URL serves it as an attachment with the name the report was stored under, and each URL issued is written to the audit trail with the purpose report-download. Do not store the URL — ask for a new one when it expires. 409 FILE_NOT_AVAILABLE_CONFLICT means the file is still being verified; 409 FILE_PURGED_CONFLICT means retention already collected it.

Execution History ​

Query past report executions with filters:

bash
GET /api/v1/reports/history?templateId=monthly-sales&status=3&format=1&pageNumber=1&pageSize=20

Query Parameters:

ParameterTypeDescription
templateIdstring?Filter by template
statusint?Filter by status (1=Queued, 2=Generating, 3=Completed, 4=Failed, 5=Delivered, 6=Cancelled)
formatint?Filter by format (1=PDF, 2=Excel, 3=CSV, 4=HTML)
fromDateDateTime?Start of date range
toDateDateTime?End of date range
pageNumberintPage number (default: 1)
pageSizeintPage size (default: 20)

Delete ​

Soft-delete a report execution and release its stored file (GrydFiles purges it after the retention grace period):

bash
DELETE /api/v1/reports/{id}

Returns 204 No Content on success.

Size Limits ​

Configure automatic async fallback for large reports:

csharp
options.MaxSyncGenerationSize = 50 * 1024 * 1024; // 50 MB max for sync
options.ForceAsyncAboveBytes = 10 * 1024 * 1024;   // Force async above 10 MB
options.SyncGenerationTimeout = TimeSpan.FromMinutes(2);

ReportOutput ​

The output object returned from generation:

csharp
public sealed class ReportOutput : IDisposable
{
    public Stream Content { get; init; }         // File content stream
    public string FileName { get; init; }        // e.g. "report.pdf"
    public string ContentType { get; init; }     // e.g. "application/pdf"
    public ReportFormat Format { get; init; }    // Pdf, Excel, Csv, Html
    public long FileSizeBytes { get; init; }     // Size in bytes
    public ReportMetadata Metadata { get; init; } // Title, Author, etc.
    public TimeSpan GenerationDuration { get; init; }

    public void Dispose() => Content?.Dispose();
}

Resource Management

ReportOutput implements IDisposable. Always dispose when using the output directly:

csharp
using var output = (await generator.GenerateAsync<SalesData, SalesParams>(
    "sales", parameters, ReportFormat.Pdf)).Data!;
// Use output.Content stream...

Released under the MIT License.