EchoSpec 1.0.1
dotnet add package EchoSpec --version 1.0.1
NuGet\Install-Package EchoSpec -Version 1.0.1
<PackageReference Include="EchoSpec" Version="1.0.1" />
<PackageVersion Include="EchoSpec" Version="1.0.1" />
<PackageReference Include="EchoSpec" />
paket add EchoSpec --version 1.0.1
#r "nuget: EchoSpec, 1.0.1"
#:package EchoSpec@1.0.1
#addin nuget:?package=EchoSpec&version=1.0.1
#tool nuget:?package=EchoSpec&version=1.0.1
EchoSpec
A flexible, generic reporting library for .NET that generates beautiful test reports in multiple output formats from a single source of data.
Features
- 📊 Multi-Format Output: Generate reports in multiple formats (Console, Markdown, HTML, or custom)
- 🎨 Beautiful Formatting: Box-drawing characters for console, proper tables for markdown, styled HTML output
- 🔧 Fully Generic: Works with any data type using
ReportBuilder<T> - 🔌 Extensible Renderers: Interface-based design allows custom output formats (JSON, CSV, etc.)
- 🚀 Zero Dependencies: Standalone library with no external dependencies
- 📝 Type-Safe: Leverages C# generics and LINQ for type safety
Installation
Via NuGet Package Manager
dotnet add package EchoSpec
Via Package Manager Console
Install-Package EchoSpec
Via PackageReference
<ItemGroup>
<PackageReference Include="EchoSpec" Version="1.0.0" />
</ItemGroup>
Quick Start
Simple Table
using EchoSpec;
var table = new TableBuilder()
.AddHeader("Name")
.AddHeader("Score", ColumnAlignment.Right)
.AddHeader("Status")
.AddRow("Alice", "95", "✓")
.AddRow("Bob", "87", "✓")
.AddRow("Charlie", "72", "~");
// Console output
Console.WriteLine(table.Build(ReportFormat.Console));
// Markdown output
File.WriteAllText("results.md", table.Build(ReportFormat.Markdown));
// HTML output
var html = (string)table.BuildWith(new HtmlRenderer());
File.WriteAllText("results.html", html);
Console Output:
Name Score Status
────────────────────────
Alice 95 ✓
Bob 87 ✓
Charlie 72 ~
Markdown Output:
| Name | Score | Status |
| ------- | ----: | ------ |
| Alice | 95 | ✓ |
| Bob | 87 | ✓ |
| Charlie | 72 | ~ |
HTML Output:
<table>
<thead>
<tr>
<th>Name</th>
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>95</td>
<td>✓</td>
</tr>
<tr>
<td>Bob</td>
<td>87</td>
<td>✓</td>
</tr>
<tr>
<td>Charlie</td>
<td>72</td>
<td>~</td>
</tr>
</tbody>
</table>
Generic Report Builder
using EchoSpec;
// Your data model
public record TestResult
{
public string TestName { get; init; }
public bool Passed { get; init; }
public TimeSpan Duration { get; init; }
}
// Collect results
var results = new List<TestResult>
{
new() { TestName = "Login Test", Passed = true, Duration = TimeSpan.FromMilliseconds(250) },
new() { TestName = "API Test", Passed = true, Duration = TimeSpan.FromMilliseconds(150) },
new() { TestName = "UI Test", Passed = false, Duration = TimeSpan.FromMilliseconds(500) }
};
// Build report
var report = new ReportBuilder<TestResult>()
.WithTitle("Test Execution Report")
.WithReferenceUrl("https://example.com/test-suite")
.AddTable((table, data) =>
{
table.AddHeader("Test Name")
.AddHeader("Status", ColumnAlignment.Center)
.AddHeader("Duration (ms)", ColumnAlignment.Right);
foreach (var result in data)
{
table.AddRow(
result.TestName,
result.Passed ? "✓" : "✗",
$"{result.Duration.TotalMilliseconds:F0}"
);
}
return table;
})
.AddSection(
// Console formatter
data => $"Total: {data.Count()} tests, {data.Count(r => r.Passed)} passed",
// Markdown formatter
data => $"**Total:** {data.Count()} tests, {data.Count(r => r.Passed)} passed"
)
.Generate(results, ReportFormat.Both);
// Display and save
report.WriteToConsole();
report.SaveMarkdown("test-report.md");
Generated Report:
Console:
╔══════════════════════════════════════════════════════╗
║ Test Execution Report ║
║ https://example.com/test-suite ║
╚══════════════════════════════════════════════════════╝
Test Name Status Duration (ms)
──────────────────────────────────
Login Test ✓ 250
API Test ✓ 150
UI Test ✗ 500
Total: 3 tests, 2 passed
Markdown:
# Test Execution Report
Reference: https://example.com/test-suite
## Test Results
| Test Name | Status | Duration (ms) |
| ---------- | :----: | ------------: |
| Login Test | ✓ | 250 |
| API Test | ✓ | 150 |
| UI Test | ✗ | 500 |
**Total:** 3 tests, 2 passed
HTML:
<h1>Test Execution Report</h1>
<p>
Reference:
<a href="https://example.com/test-suite">https://example.com/test-suite</a>
</p>
<h2>Test Results</h2>
<table>
<thead>
<tr>
<th>Test Name</th>
<th>Status</th>
<th>Duration (ms)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Login Test</td>
<td>✓</td>
<td>250</td>
</tr>
<tr>
<td>API Test</td>
<td>✓</td>
<td>150</td>
</tr>
<tr>
<td>UI Test</td>
<td>✗</td>
<td>500</td>
</tr>
</tbody>
</table>
<p><strong>Total:</strong> 3 tests, 2 passed</p>
Core Components
Renderer Interfaces
EchoSpec uses an interface-based design for extensibility:
public interface ITableRenderer
{
string Name { get; }
string Render(ITable table);
}
public interface IReportRenderer
{
string Name { get; }
string RenderTitle(string title, string? referenceUrl = null);
string RenderSectionHeader(string sectionTitle);
string RenderTable(ITable table);
string RenderText(string text);
string CombineParts(IEnumerable<string> parts);
}
Built-in Renderers:
ConsoleRenderer- Box-drawing characters for terminal outputMarkdownRenderer- GitHub-compatible markdown tablesHtmlRenderer- Complete HTML documents with embedded CSS styling
ReportFormat Enum
public enum ReportFormat
{
Console, // Console output only
Markdown, // Markdown output only
Both // Generate both formats
}
TableBuilder
Builds formatted tables for any renderer:
var table = new TableBuilder()
.AddHeader("Column 1", ColumnAlignment.Left)
.AddHeader("Column 2", ColumnAlignment.Right)
.AddRow("value1", "value2")
.Build(ReportFormat.Console);
// Or use a custom renderer directly
var customRenderer = new HtmlRenderer();
var html = (string)table.BuildWith(customRenderer);
// Or generate multiple formats at once
var outputs = (Dictionary<string, string>)table.BuildWith(
new ConsoleRenderer(),
new MarkdownRenderer(),
new HtmlRenderer()
);
Console.WriteLine(outputs["Console"]);
File.WriteAllText("report.md", outputs["Markdown"]);
File.WriteAllText("report.html", outputs["HTML"]);
Methods:
AddHeader(string, ColumnAlignment): Add column headerAddRow(params object[]): Add data rowBuild(ReportFormat): Generate output using built-in renderersBuildWith(params ITableRenderer[]): Generate output using one or more custom renderers- Single renderer returns
string - Multiple renderers return
Dictionary<string, string>keyed by renderer name
- Single renderer returns
Clear(): Reset builder state
ReportBuilder<T>
Generic report generator that works with any data type:
var report = new ReportBuilder<MyDataType>()
.WithTitle("Report Title")
.WithReferenceUrl("https://...")
.AddTable((builder, data) => { /* configure table */ })
.AddSection(consoleFormatter, markdownFormatter)
.Generate(dataList, ReportFormat.Both);
Methods:
WithTitle(string): Set report titleWithReferenceUrl(string): Add reference URLAddTable(Func<TableBuilder, IEnumerable<T>, TableBuilder>, string?): Add table sectionAddSection(Func<IEnumerable<T>, string>, Func<IEnumerable<T>, string>, string?): Add custom sectionGenerate(IEnumerable<T>, ReportFormat): Generate report using built-in renderersGenerateWith(IEnumerable<T>, params IReportRenderer[]): Generate report using one or more custom renderers- Single renderer returns
string - Multiple renderer returns
Dictionary<string, string>keyed by renderer name
- Single renderer returns
ReportOutput
Container for generated reports:
public class ReportOutput
{
public string? ConsoleText { get; }
public string? MarkdownText { get; }
public void WriteToConsole();
public void SaveMarkdown(string filePath);
}
Advanced Examples
Multi-Section Report
var report = new ReportBuilder<BenchmarkResult>()
.WithTitle("Performance Benchmarks")
.AddTable((table, data) =>
{
table.AddHeader("Operation")
.AddHeader("Time (ms)", ColumnAlignment.Right)
.AddHeader("Memory (MB)", ColumnAlignment.Right);
foreach (var result in data.OrderBy(r => r.Time))
{
table.AddRow(result.Operation, $"{result.Time:F2}", $"{result.Memory:F1}");
}
return table;
}, "Detailed Results")
.AddSection(
data => $"Fastest: {data.MinBy(r => r.Time)?.Operation} ({data.Min(r => r.Time):F2}ms)",
data => $"**Fastest:** {data.MinBy(r => r.Time)?.Operation} ({data.Min(r => r.Time):F2}ms)",
"Summary"
)
.Generate(benchmarks, ReportFormat.Both);
Custom Formatting
var report = new ReportBuilder<ErrorLog>()
.WithTitle("Error Report")
.AddTable((table, data) =>
{
table.AddHeader("Timestamp")
.AddHeader("Level")
.AddHeader("Message");
foreach (var error in data.OrderByDescending(e => e.Timestamp))
{
var level = error.Level == LogLevel.Error ? "❌" : "⚠️";
table.AddRow(
error.Timestamp.ToString("HH:mm:ss"),
level,
error.Message.Length > 50
? error.Message[..47] + "..."
: error.Message
);
}
return table;
})
.Generate(errorLogs, ReportFormat.Console);
report.WriteToConsole();
Multiple Renderers at Once
Generate all formats in a single call:
var table = new TableBuilder()
.AddHeader("Feature")
.AddHeader("Status", ColumnAlignment.Center)
.AddRow("Authentication", "✓")
.AddRow("API Integration", "✓")
.AddRow("UI Polish", "⏳");
// Generate console, markdown, and HTML simultaneously
var outputs = (Dictionary<string, string>)table.BuildWith(
new ConsoleRenderer(),
new MarkdownRenderer(),
new HtmlRenderer(),
new CsvRenderer()
);
// Use each format as needed
Console.WriteLine(outputs["Console"]);
File.WriteAllText("status.md", outputs["Markdown"]);
File.WriteAllText("status.html", outputs["HTML"]);
// Or with reports
var report = new ReportBuilder<FeatureStatus>()
.WithTitle("Feature Status Report")
.AddTable((table, data) => { /* ... */ });
var reportOutputs = (Dictionary<string, string>)report.GenerateWith(
features,
new ConsoleRenderer(),
new MarkdownRenderer(),
new HtmlRenderer()
);
Design Principles
- Single Source of Truth: Data collected once, formatted multiple ways
- Format Flexibility: Easy to extend with new output formats (HTML, JSON, etc.)
- Type Safety: Leverage C# generics for compile-time safety
- Separation of Concerns: Data collection separated from presentation
- Developer Experience: Fluent API for readable code
Extending EchoSpec
Creating Custom Renderers
EchoSpec's interface-based design makes it easy to add new output formats. The library includes three built-in renderers (ConsoleRenderer, MarkdownRenderer, HtmlRenderer) as reference implementations.
Here's how to create a custom CSV renderer:
using EchoSpec;
using System.Text;
public class CsvRenderer : ITableRenderer, IReportRenderer
{
public string Name => "CSV";
public string Render(ITable table)
{
var sb = new StringBuilder();
// Header row
sb.AppendLine(string.Join(",", table.Headers.Select(EscapeCsv)));
// Data rows
foreach (var row in table.Rows)
{
var cells = row.Select(EscapeCsv);
sb.AppendLine(string.Join(",", cells));
}
return sb.ToString();
}
public string RenderTitle(string title, string? referenceUrl = null)
{
var sb = new StringBuilder();
sb.AppendLine($"# {title}");
if (referenceUrl != null)
{
sb.AppendLine($"# Reference: {referenceUrl}");
}
sb.AppendLine();
return sb.ToString();
}
public string RenderSectionHeader(string sectionTitle)
{
return $"\n# {sectionTitle}\n";
}
public string RenderTable(ITable table) => Render(table);
public string RenderText(string text)
{
return $"# {text}\n";
}
public string CombineParts(IEnumerable<string> parts)
{
return string.Join("\n", parts.Where(p => !string.IsNullOrWhiteSpace(p)));
}
private static string EscapeCsv(string value)
{
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
{
return $"\"{value.Replace("\"", "\"\"")}\"";
}
return value;
}
}
Usage:
// Use with TableBuilder
var csv = (string)table.BuildWith(new CsvRenderer());
File.WriteAllText("data.csv", csv);
// Use with ReportBuilder
var report = new ReportBuilder<TestResult>()
.WithTitle("Test Report")
.AddTable((table, data) => { /* ... */ })
.GenerateWith(results, new CsvRenderer());
File.WriteAllText("report.csv", (string)report);
// Generate multiple formats at once
var outputs = (Dictionary<string, string>)table.BuildWith(
new ConsoleRenderer(),
new MarkdownRenderer(),
new HtmlRenderer(),
new CsvRenderer()
);
File.WriteAllText("data.html", outputs["HTML"]);
File.WriteAllText("data.csv", outputs["CSV"]);
Other potential custom renderers:
- JsonRenderer - Export to JSON format
- XmlRenderer - Export to XML
- AnsiRenderer - Rich console colors with ANSI codes
- LatexRenderer - Academic papers and documents
- SqlRenderer - Generate INSERT statements
- ExcelRenderer - XLSX format (requires external library)
Performance
- Overhead: <1ms per report generation
- Memory: Minimal allocations, single-pass generation
- Scalability: Handles thousands of rows efficiently
Building & Publishing
Build the Package
cd src/EchoSpec
dotnet build -c Release
Create NuGet Package
dotnet pack -c Release -o ./nupkg
Local Testing
# Add local package source
dotnet nuget add source /path/to/nupkg --name LocalEchoSpec
# Install from local source
dotnet add package EchoSpec --source LocalEchoSpec
Contributing
Contributions welcome!
Development Setup
- Clone the repository
- Open in Visual Studio 2022 or VS Code with C# extension
- Build:
dotnet build - Run tests:
dotnet test
Guidelines
- Follow existing code style and patterns
- Add XML documentation for public APIs
- Update README.md and CHANGELOG.md
- Ensure zero external dependencies
- Add examples for new features
License
MIT License - See LICENSE file for details
EchoSpec - Echo your specs beautifully 📊✨
| Product | Versions 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
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 | 306 | 11/11/2025 |
| 1.0.1-beta.4 | 248 | 11/11/2025 |
| 1.0.0 | 299 | 11/11/2025 |
| 1.0.0-beta.3 | 248 | 11/11/2025 |
| 1.0.0-beta.1 | 249 | 11/11/2025 |
Initial release with extensible renderer architecture. Includes built-in Console, Markdown, and HTML renderers with support for custom formats.