Appearance
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}/downloadResponse 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=20Query Parameters:
| Parameter | Type | Description |
|---|---|---|
templateId | string? | Filter by template |
status | int? | Filter by status (1=Queued, 2=Generating, 3=Completed, 4=Failed, 5=Delivered, 6=Cancelled) |
format | int? | Filter by format (1=PDF, 2=Excel, 3=CSV, 4=HTML) |
fromDate | DateTime? | Start of date range |
toDate | DateTime? | End of date range |
pageNumber | int | Page number (default: 1) |
pageSize | int | Page 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...