Appearance
Report Templates
Templates are the heart of GrydReports. They define the structure, metadata, and supported formats for each report type using a code-first approach.
IReportTemplate<TData>
Every template implements IReportTemplate<TData>:
csharp
public interface IReportTemplate<in TData>
{
string TemplateId { get; } // Unique identifier
string DisplayName { get; } // Human-readable name
string? Description { get; } // Optional description
IReadOnlyList<ReportFormat> SupportedFormats { get; } // PDF, Excel, CSV, HTML
IReadOnlyList<ReportParameterDefinition> Parameters { get; } // Input params
PageSettings PageSettings { get; } // Page layout (default A4)
ReportMetadata GetMetadata(TData data); // Dynamic metadata
}Basic Template
csharp
public class MonthlyBillingTemplate : IReportTemplate<BillingData>
{
public string TemplateId => "monthly-billing";
public string DisplayName => "Monthly Billing Report";
public string? Description => "Customer billing summary by month";
public IReadOnlyList<ReportFormat> SupportedFormats =>
[ReportFormat.Pdf, ReportFormat.Excel, ReportFormat.Csv];
public IReadOnlyList<ReportParameterDefinition> Parameters =>
[
new("month", "Month", "int", required: true),
new("year", "Year", "int", required: true),
new("customerId", "Customer ID", "guid", required: false)
];
public ReportMetadata GetMetadata(BillingData data) => new()
{
Title = $"Billing - {data.MonthName} {data.Year}",
Author = "Finance Department",
Subject = "Monthly Billing Summary",
Keywords = "billing, finance, monthly"
};
}Parameters
Define typed input parameters with validation and defaults:
csharp
public IReadOnlyList<ReportParameterDefinition> Parameters =>
[
// Required string parameter
new("customerId", "Customer", "guid", required: true),
// Optional with default
new("format", "Output Format", "string", required: false,
defaultValue: "detailed"),
// With allowed values (dropdown in UIs)
new("status", "Status Filter", "string", required: false,
allowedValues: ["active", "inactive", "all"],
defaultValue: "active"),
// Date range
new("fromDate", "Start Date", "datetime", required: true),
new("toDate", "End Date", "datetime", required: true),
// Numeric parameter
new("minAmount", "Minimum Amount", "decimal", required: false,
defaultValue: 0m,
description: "Filter transactions above this amount")
];Page Settings
Control page layout for renderers that support it:
csharp
public PageSettings PageSettings => new()
{
Size = PageSize.A4,
Orientation = PageOrientation.Landscape,
MarginTop = 20,
MarginBottom = 20,
MarginLeft = 25,
MarginRight = 25
};Data Sources
Standard Data Source
For queries that return a complete dataset:
csharp
public class BillingDataSource : IReportDataSource<BillingData, BillingParameters>
{
private readonly IBillingRepository _billing;
public BillingDataSource(IBillingRepository billing) => _billing = billing;
public async Task<BillingData> FetchDataAsync(
BillingParameters parameters, CancellationToken ct)
{
var invoices = await _billing.GetByPeriodAsync(
parameters.Year, parameters.Month, ct);
return new BillingData
{
Year = parameters.Year,
MonthName = new DateTime(parameters.Year, parameters.Month, 1).ToString("MMMM"),
Invoices = invoices,
TotalAmount = invoices.Sum(i => i.Total)
};
}
// Optional: validate parameters before execution
public async Task<Result> ValidateParametersAsync(
BillingParameters parameters, CancellationToken ct)
{
if (parameters.Month < 1 || parameters.Month > 12)
return Result.Failure("Month must be between 1 and 12");
if (parameters.Year < 2020 || parameters.Year > DateTime.UtcNow.Year)
return Result.Failure("Year is out of range");
return Result.Success();
}
}Streaming Data Source
For large datasets that should be streamed (avoids loading everything into memory):
csharp
public class TransactionStreamSource
: IStreamingReportDataSource<TransactionRecord, TransactionParams>
{
private readonly ITransactionRepository _repo;
public TransactionStreamSource(ITransactionRepository repo) => _repo = repo;
public async IAsyncEnumerable<TransactionRecord> StreamDataAsync(
TransactionParams parameters,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var tx in _repo.StreamByDateRangeAsync(
parameters.FromDate, parameters.ToDate, ct))
{
yield return new TransactionRecord
{
Id = tx.Id,
Date = tx.Date,
Amount = tx.Amount,
Description = tx.Description
};
}
}
public async Task<long> GetEstimatedCountAsync(
TransactionParams parameters, CancellationToken ct)
{
return await _repo.CountByDateRangeAsync(
parameters.FromDate, parameters.ToDate, ct);
}
}Multi-Format Templates
Templates can support multiple output formats. The renderer is selected at generation time:
csharp
public class ProductCatalogTemplate : IReportTemplate<CatalogData>
{
public string TemplateId => "product-catalog";
public string DisplayName => "Product Catalog";
public IReadOnlyList<ReportFormat> SupportedFormats =>
[ReportFormat.Pdf, ReportFormat.Excel, ReportFormat.Csv, ReportFormat.Html];
// ...
}Each format is rendered by its respective IReportRenderer:
| Format | Renderer | Package | Best For |
|---|---|---|---|
Pdf | QuestPDF | GrydReports.Infrastructure.QuestPdf | Printable documents with layouts |
Excel | ClosedXML | GrydReports.Infrastructure.ClosedXml | Spreadsheets with formulas |
Csv | CsvHelper | GrydReports.Infrastructure.CsvHelper | Data exports, ETL pipelines |
Html | Built-in | GrydReports.Core | Web previews, email embeds |
Registration
Auto-Discovery
With AutoRegisterTemplates = true (default), templates are discovered automatically from assemblies:
csharp
opts.AutoRegisterTemplates = true;
opts.TemplateAssemblies.Add(typeof(MyTemplate).Assembly);Manual Registration
csharp
builder.Services.AddSingleton<IReportTemplate<BillingData>, MonthlyBillingTemplate>();
builder.Services.AddScoped<IReportDataSource<BillingData, BillingParams>, BillingDataSource>();Template Best Practices
DRY Templates
Extract common metadata logic into a base class:
csharp
public abstract class CompanyReportTemplate<TData> : IReportTemplate<TData>
{
public abstract string TemplateId { get; }
public abstract string DisplayName { get; }
public ReportMetadata GetMetadata(TData data) => new()
{
Author = "Company Name",
Keywords = "company, report",
Title = GetTitle(data)
};
protected abstract string GetTitle(TData data);
}Performance
For data sources that aggregate large datasets, prefer IStreamingReportDataSource to avoid out-of-memory issues. The streaming interface yields records one-by-one via IAsyncEnumerable.