SimpleReportGenerator 1.0.1

dotnet add package SimpleReportGenerator --version 1.0.1
                    
NuGet\Install-Package SimpleReportGenerator -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SimpleReportGenerator" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SimpleReportGenerator" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="SimpleReportGenerator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SimpleReportGenerator --version 1.0.1
                    
#r "nuget: SimpleReportGenerator, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SimpleReportGenerator@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SimpleReportGenerator&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=SimpleReportGenerator&version=1.0.1
                    
Install as a Cake Tool

SimpleReportGenerator

Minimal Blazor report table component with optional grouping.

Install

dotnet add package SimpleReportGenerator

Quick start

<ReportGenerator Data="items">
    <TableHeader>
        <tr>
            <th>Name</th>
            <th>Total</th>
        </tr>
    </TableHeader>
    <RowTemplate Context="row">
        <tr>
            <td>@row.Name</td>
            <td>@row.Total</td>
        </tr>
    </RowTemplate>
</ReportGenerator>

ReportGenerator Component

A generic Blazor component for generating HTML table-based reports with support for grouping, aggregation, headers, footers, and detail rows.

Overview

The ReportGenerator<TItem> component provides a declarative way to create grouped reports in Blazor applications. It supports:

  • Detail rows for displaying individual data records
  • Row templates for simple row rendering without a context wrapper
  • Group header that displays when a group begins (one template for all levels)
  • Group footer that displays when a group ends with aggregated values (one template for all levels)
  • Multiple grouping levels (nested groups)
  • Automatic aggregation of numeric values
  • Custom aggregation and reset logic
  • Level-aware templates - use context.Level to conditionally render different content per level

Basic Usage

@page "/my-report"

<ReportGenerator TItem="SalesData" 
                 Data="@salesData"
                 GroupBreaks="@groupBreaks">
    <TableHeader>
        <tr>
            <th>Region</th>
            <th>Product</th>
            <th class="text-end">Amount</th>
        </tr>
    </TableHeader>
    <DetailRow Context="ctx">
        <td>@ctx.Row.Region</td>
        <td>@ctx.Row.Product</td>
        <td class="text-end">@ctx.Row.Amount.ToString("C")</td>
    </DetailRow>
    <GroupHeader Context="ctx">
        <td colspan="3">
            <strong>Region: @ctx.Aggregate.Region</strong>
        </td>
    </GroupHeader>
    <GroupFooter Context="ctx">
        @if (ctx.Level == 0)
        {
            <td colspan="2"><strong>Grand Total:</strong></td>
            <td class="text-end"><strong>@ctx.Aggregate.Amount.ToString("C")</strong></td>
        }
        else
        {
            <td colspan="2">Region Total:</td>
            <td class="text-end">@ctx.Aggregate.Amount.ToString("C")</td>
        }
    </GroupFooter>
</ReportGenerator>

@code {
    private List<SalesData> salesData = new();
    
    private Dictionary<int, Func<SalesData, SalesData, bool>> groupBreaks = new()
    {
        { 0, (prev, curr) => false }, // Grand total - breaks on last row automatically
        { 1, (prev, curr) => prev.Region != curr.Region } // Break when region changes
    };

    public class SalesData
    {
        public string Region { get; set; } = string.Empty;
        public string Product { get; set; } = string.Empty;
        public decimal Amount { get; set; }
    }
}

Group Levels

Group levels determine the nesting hierarchy of headers and footers:

Level Description
0 Grand totals - only triggers on the last row
1 Innermost group - immediately surrounds detail rows
2 Next outer group - surrounds Level 1
3 Surrounds Level 2
... And so on
Level 2 Header (context.Level == 2)
    Level 1 Header (context.Level == 1)
        Detail Row
        Detail Row
    Level 1 Footer (context.Level == 1)
    Level 1 Header (context.Level == 1)
        Detail Row
    Level 1 Footer (context.Level == 1)
Level 2 Footer (context.Level == 2)
Level 0 Footer (context.Level == 0, Grand Totals)

Parameters

Parameter Type Default Description
Data IEnumerable<TItem>? Required The data source for the report (alias: Items)
Items IEnumerable<TItem>? Required Alternate name for Data
TableCssClass string? null CSS classes for the <table> element (alias: TableClass)
TableClass string "table table-sm table-striped table-bordered w-auto" Default table CSS when TableCssClass is not provided
TableHeader RenderFragment? null Template for the <thead> section (alias: ReportHeader)
ReportHeader RenderFragment? null Alternate name for TableHeader
DetailRow RenderFragment<ReportDetailContext<TItem>>? null Template for each detail row
RowTemplate RenderFragment<TItem>? null Simpler row template without context wrapper
GroupHeader RenderFragment<ReportGroupContext<TItem>>? null Single template for all group headers
GroupFooter RenderFragment<ReportGroupContext<TItem>>? null Single template for all group footers
NoRecordsTemplate RenderFragment? null Template displayed when there are no records (alias: EmptyTemplate)
EmptyTemplate RenderFragment? null Alternate name for NoRecordsTemplate
GroupAggregate Action<TItem, TItem>? null Custom aggregation function
GroupReset Action<TItem>? null Custom reset function for aggregates
GroupBreaks Dictionary<int, Func<TItem, TItem, bool>>? null Functions that determine when groups break
ReportName string "Report" Used by exporters (if any)

Context Objects

ReportDetailContext<TItem>

Passed to the DetailRow template:

Property Type Description
Row TItem The current detail row data
GroupStyle string CSS style for group indentation

ReportGroupContext<TItem>

Passed to GroupHeader and GroupFooter templates:

Property Type Description
Aggregate TItem The aggregate object for this group level
Row TItem The current detail row (for reference)
Level int The group level being rendered (0 = grand total, 1+ = group levels)
GroupStyle string CSS style for group indentation

Using context.Level for Conditional Rendering

Since the header and footer templates are called for each breaking level, use context.Level to customize the output:

<GroupHeader Context="ctx">
    @switch (ctx.Level)
    {
        case 2:
            <td colspan="3" class="table-primary"><strong>Region: @ctx.Aggregate.Region</strong></td>
            break;
        case 1:
            <td colspan="3" style="@ctx.GroupStyle">Department: @ctx.Aggregate.Department</td>
            break;
    }
</GroupHeader>

<GroupFooter Context="ctx">
    @{
        var label = ctx.Level switch
        {
            0 => "Grand Total:",
            1 => "Department Total:",
            2 => "Region Total:",
            _ => $"Level {ctx.Level} Total:"
        };
    }
    <td colspan="2" style="@ctx.GroupStyle">@label</td>
    <td class="text-end">@ctx.Aggregate.Salary.ToString("C")</td>
</GroupFooter>

Aggregation

Default Aggregation

When no GroupAggregate function is provided, the component uses default aggregation:

  • Numeric types (int, long, double, decimal, etc.): Values are summed
  • Non-numeric types (string, DateTime, etc.): Values are copied from the detail row to the aggregate

Custom Aggregation

Provide a custom GroupAggregate function for more control:

<ReportGenerator TItem="SalesData" 
                 Data="@data"
                 GroupAggregate="@AggregateData"
                 GroupReset="@ResetAggregate">
    ...
</ReportGenerator>

@code {
    private void AggregateData(SalesData aggregate, SalesData detail)
    {
        aggregate.TotalAmount += detail.Amount;
        aggregate.ItemCount++;
        aggregate.AverageAmount = aggregate.TotalAmount / aggregate.ItemCount;
        aggregate.Region = detail.Region; // Copy grouping field
    }

    private void ResetAggregate(SalesData aggregate)
    {
        aggregate.TotalAmount = 0;
        aggregate.ItemCount = 0;
        aggregate.AverageAmount = 0;
    }
}

Group Breaks

The GroupBreaks dictionary defines when groups should break. Each entry maps a level to a function that compares the previous and current rows:

private Dictionary<int, Func<MyData, MyData, bool>> groupBreaks = new()
{
    // Level 0: Grand totals - automatically breaks on last row
    { 0, (prev, curr) => false },
    
    // Level 1: Break when Department changes
    { 1, (prev, curr) => prev.Department != curr.Department },
    
    // Level 2: Break when Region changes
    { 2, (prev, curr) => prev.Region != curr.Region }
};

Important:

  • Level 0 always triggers a footer on the last row (grand totals)
  • Headers at a level trigger when any break at that level or lower occurs
  • Footers at a level trigger when any break at that level or lower occurs

Multi-Level Grouping Example

<ReportGenerator TItem="EmployeeData" 
                 Data="@employees"
                 GroupBreaks="@groupBreaks"
                 TableCssClass="table table-sm table-bordered">
    <TableHeader>
        <tr class="table-dark">
            <th>Region</th>
            <th>Department</th>
            <th>Employee</th>
            <th class="text-end">Salary</th>
        </tr>
    </TableHeader>
    <DetailRow Context="ctx">
        <td>@ctx.Row.Region</td>
        <td>@ctx.Row.Department</td>
        <td>@ctx.Row.Name</td>
        <td class="text-end">@ctx.Row.Salary.ToString("C")</td>
    </DetailRow>
    <GroupHeader Context="ctx">
        @if (ctx.Level == 2)
        {
            <td colspan="4" class="table-primary"><strong>Region: @ctx.Aggregate.Region</strong></td>
        }
        else if (ctx.Level == 1)
        {
            <td colspan="4" class="table-secondary"><strong>Department: @ctx.Aggregate.Department</strong></td>
        }
    </GroupHeader>
    <GroupFooter Context="ctx">
        @if (ctx.Level == 1)
        {
            <td colspan="3">Department Total</td>
            <td class="text-end">@ctx.Aggregate.Salary.ToString("C")</td>
        }
        else if (ctx.Level == 2)
        {
            <td colspan="3"><strong>Region Total</strong></td>
            <td class="text-end"><strong>@ctx.Aggregate.Salary.ToString("C")</strong></td>
        }
        else if (ctx.Level == 0)
        {
            <td colspan="3"><strong>Grand Total</strong></td>
            <td class="text-end"><strong>@ctx.Aggregate.Salary.ToString("C")</strong></td>
        }
    </GroupFooter>
</ReportGenerator>

@code {
    private List<EmployeeData> employees = new();

    private Dictionary<int, Func<EmployeeData, EmployeeData, bool>> groupBreaks = new()
    {
        { 0, (prev, curr) => false },
        { 1, (prev, curr) => prev.Department != curr.Department },
        { 2, (prev, curr) => prev.Region != curr.Region }
    };

    public class EmployeeData
    {
        public string Region { get; set; } = string.Empty;
        public string Department { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
        public decimal Salary { get; set; }
    }
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 65 3/12/2026
1.0.0 64 3/9/2026