Building a flexible reporting system is one of the most recurring challenges in enterprise applications. Different departments need different formats, filters, rules, and visual outputs. Hard-coding reports creates long maintenance cycles and slows down delivery.
A Metadata-driven Reporting Architecture solves this problem. Instead of writing code for every report, we define reports using metadata (stored in a database), and the system automatically generates:
This approach allows non-developers (admins, analysts) to configure and publish new reports without modifying backend or frontend code.
Why Metadata-Driven Reporting?
Traditional reporting involves:
Developers writing SQL queries.
Deployments for every report change.
Difficult scaling when the number of reports reaches hundreds.
A metadata-based approach offers:
No deployment required for new reports.
Dynamic filtering, sorting, pagination.
Data-level access control (tenant, role, user).
Standardized performance and caching rules.
Core Concepts
A metadata-driven report framework includes:
| Layer | Purpose |
|---|
| Metadata Storage | Defines report structure, filters, joins, fields |
| Query Engine | Converts metadata into executable SQL |
| API Layer | Runs the queries securely and efficiently |
| Frontend Renderer | Auto-builds UI (table or chart) from metadata |
| Access Rules | Controls visibility per user or role |
Architecture Overview
Metadata DB
|
v
Query Builder Engine
|
v
API Execution and Caching
|
v
Angular UI Renderer (Table/Chart/Export)
Workflow Diagram
User Requests Report
|
vLoad Report Metadata
|
vBuild SQL Using Query Builder
|
vExecute and Cache Results
|
vRender UI Auto (Grid or Chart)
|
vOptional Export (CSV, PDF, Excel)
Flowchart
START|
v
Select Report from UI
|
v
Fetch Metadata from DB
|
v
Metadata valid?
|--NO--> Show error|
YES
|
v
Generate SQL Query dynamically
|
v
Execute Query with filters
|
v
Return paginated result set|
v
Render table or chart dynamically
|END
Metadata Model Design
Report Table
CREATE TABLE Reports (
ReportId UNIQUEIDENTIFIER PRIMARY KEY,
Name NVARCHAR(200),
Description NVARCHAR(MAX),
BaseTable NVARCHAR(200),
ChartType NVARCHAR(50) NULL,
IsActive BIT DEFAULT 1
);
Field Metadata
CREATE TABLE ReportFields (
FieldId UNIQUEIDENTIFIER PRIMARY KEY,
ReportId UNIQUEIDENTIFIER,
ColumnName NVARCHAR(200),
DisplayName NVARCHAR(200),
DataType NVARCHAR(50),
IsVisible BIT DEFAULT 1,
IsSortable BIT DEFAULT 1,
IsFilterable BIT DEFAULT 1,
Aggregation NVARCHAR(100) NULL
);
Filter Metadata
CREATE TABLE ReportFilters (
FilterId UNIQUEIDENTIFIER PRIMARY KEY,
ReportId UNIQUEIDENTIFIER,
FieldName NVARCHAR(200),
FilterType NVARCHAR(50), -- textbox, dropdown, dateRange
DefaultValue NVARCHAR(200) NULL
);
Dynamic SQL Generation (C# Sample)
public string BuildQuery(ReportDefinition report, Dictionary<string, string> filters)
{
var select = string.Join(", ",
report.Fields.Where(f => f.IsVisible).Select(f => f.ColumnName));
var sql = $"SELECT {select} FROM {report.BaseTable}";
if(filters.Any())
{
var where = string.Join(" AND ",
filters.Select(f => $"{f.Key} = @{f.Key}"));
sql += $" WHERE {where}";
}
return sql;
}
API Endpoint
[HttpPost("execute")]
public async Task<IActionResult> Execute([FromBody] ReportRequest request)
{
var metadata = await _metadataService.GetReport(request.ReportId);
var sql = _queryBuilder.BuildQuery(metadata, request.Filters);
var result = await _db.QueryAsync(sql, request.Filters);
return Ok(result);
}
Angular UI Renderer
The frontend dynamically builds columns and filters based on metadata.
loadReport(reportId: string) {
this.http.post('/api/reports/execute', { reportId, filters: {} })
.subscribe(result => {
this.columns = result.metadata.fields;
this.rows = result.data;
});
}
Rendering Modes
The UI should automatically detect and render based on metadata:
| Mode | Trigger |
|---|
| Table | Default |
| Chart | Metadata.ChartType not null |
| Pivot View | If Aggregations exist |
| Export Mode | User clicks Export to PDF/Excel |
Access Control
Store role-based visibility rules:
CREATE TABLE ReportPermissions (
ReportId UNIQUEIDENTIFIER,
Role NVARCHAR(200),
CanExecute BIT DEFAULT 1,
CanExport BIT DEFAULT 1
);
API checks permissions before execution.
Caching Strategy
To prevent repeated heavy query execution:
Cache by ReportId + Filter hash.
Set TTL based on request frequency.
Invalidate cache when data source changes.
Performance Considerations
Real-World Use Cases
ERP with configurable dashboards.
Finance reconciliation reporting.
Manufacturing KPIs and operational metrics.
Multi-tenant SaaS usage monitoring.
Best Practices
Audit report execution logs.
Never expose raw column names to UI.
Generate preview before allowing publish.
Allow cloning and versioning of reports.
Conclusion
A metadata-driven reporting architecture allows platforms to scale reporting without repeated development cycles. With SQL generation, API abstraction, and dynamic UI rendering, business teams gain direct ownership of report creation while developers focus on core platform logic.