Skip to content

Commit 6208d43

Browse files
committed
samples(04): config-driven report (dynamic path) end-to-end
Adds sample 04-dynamic-config-csv demonstrating A2: a report defined entirely in report.json (no typed POCO) is parsed, compiled and run, writing a real CSV. The JSON drives the report name, source selection, columns/schema and output/destination selection; an in-memory IConfigSourceProvider stands in until the SQL config source (A3), and the CSV/Local factories are pre-wired in DI until config option binding (A5). Registered in the solution under the samples folder.
1 parent b3a2512 commit 6208d43

6 files changed

Lines changed: 205 additions & 0 deletions

File tree

NeoReports.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeoReports.AspNetCore.Integ
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "03-async-job-hangfire", "samples\03-async-job-hangfire\03-async-job-hangfire.csproj", "{21CDAB18-7D2A-4926-B27C-B29F82C01ECD}"
5757
EndProject
58+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "04-dynamic-config-csv", "samples\04-dynamic-config-csv\04-dynamic-config-csv.csproj", "{90351642-8368-4814-A097-D7A1241A4C43}"
59+
EndProject
5860
Global
5961
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6062
Debug|Any CPU = Debug|Any CPU
@@ -329,6 +331,18 @@ Global
329331
{21CDAB18-7D2A-4926-B27C-B29F82C01ECD}.Release|x64.Build.0 = Release|Any CPU
330332
{21CDAB18-7D2A-4926-B27C-B29F82C01ECD}.Release|x86.ActiveCfg = Release|Any CPU
331333
{21CDAB18-7D2A-4926-B27C-B29F82C01ECD}.Release|x86.Build.0 = Release|Any CPU
334+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
335+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|Any CPU.Build.0 = Debug|Any CPU
336+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|x64.ActiveCfg = Debug|Any CPU
337+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|x64.Build.0 = Debug|Any CPU
338+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|x86.ActiveCfg = Debug|Any CPU
339+
{90351642-8368-4814-A097-D7A1241A4C43}.Debug|x86.Build.0 = Debug|Any CPU
340+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|Any CPU.ActiveCfg = Release|Any CPU
341+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|Any CPU.Build.0 = Release|Any CPU
342+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|x64.ActiveCfg = Release|Any CPU
343+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|x64.Build.0 = Release|Any CPU
344+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|x86.ActiveCfg = Release|Any CPU
345+
{90351642-8368-4814-A097-D7A1241A4C43}.Release|x86.Build.0 = Release|Any CPU
332346
EndGlobalSection
333347
GlobalSection(SolutionProperties) = preSolution
334348
HideSolutionNode = FALSE
@@ -356,5 +370,6 @@ Global
356370
{DC55DB23-7998-47DC-9088-FA1B7B3821D1} = {11111111-1111-1111-1111-111111111111}
357371
{9477CA22-21CE-453F-9DC9-B99D874B2CD0} = {22222222-2222-2222-2222-222222222222}
358372
{21CDAB18-7D2A-4926-B27C-B29F82C01ECD} = {44444444-4444-4444-4444-444444444444}
373+
{90351642-8368-4814-A097-D7A1241A4C43} = {44444444-4444-4444-4444-444444444444}
359374
EndGlobalSection
360375
EndGlobal
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<IsPackable>false</IsPackable>
9+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\..\src\NeoReports.Core\NeoReports.Core.csproj" />
14+
<ProjectReference Include="..\..\src\Formats\NeoReports.Formats.Csv\NeoReports.Formats.Csv.csproj" />
15+
<ProjectReference Include="..\..\src\Destinations\NeoReports.Destinations.Local\NeoReports.Destinations.Local.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
20+
<PackageReference Include="Microsoft.Extensions.Logging" />
21+
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<None Update="report.json" CopyToOutputDirectory="PreserveNewest" />
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Globalization;
2+
using NeoReports.Abstractions;
3+
4+
namespace NeoReports.Samples.DynamicConfigCsv;
5+
6+
/// <summary>
7+
/// Stand-in <see cref="IConfigSourceProvider"/> (id "inmemory") used until the SQL config source
8+
/// lands in A3. It reads the row count from the source <c>properties</c> and emits positional
9+
/// <see cref="ReportRecord"/> rows aligned to the report schema — exactly what a real provider does,
10+
/// minus the database.
11+
/// </summary>
12+
internal sealed class InMemorySalesSourceProvider : IConfigSourceProvider
13+
{
14+
public string Type => "inmemory";
15+
16+
public IBatchSource<ReportRecord> Create(SourceConfig source, ReportSchema schema, IServiceProvider services)
17+
{
18+
var count = source.Properties is not null
19+
&& source.Properties.TryGetValue("rows", out var raw)
20+
&& raw is long n
21+
? (int)n
22+
: 5;
23+
24+
var rows = new List<ReportRecord>(count);
25+
for (var i = 1; i <= count; i++)
26+
{
27+
rows.Add(new ReportRecord(schema, new object?[]
28+
{
29+
(long)i,
30+
$"Customer {i}",
31+
i * 100.5m,
32+
new DateTime(2026, 1, 1).AddDays(i - 1),
33+
}));
34+
}
35+
36+
return new InMemoryRecordSource(schema, rows);
37+
}
38+
39+
/// <summary>Serves the pre-built records one page at a time, driven by the page number.</summary>
40+
private sealed class InMemoryRecordSource : IBatchSource<ReportRecord>
41+
{
42+
private readonly IReadOnlyList<ReportRecord> _rows;
43+
44+
public InMemoryRecordSource(ReportSchema schema, IReadOnlyList<ReportRecord> rows)
45+
{
46+
Schema = schema;
47+
_rows = rows;
48+
}
49+
50+
public ReportSchema Schema { get; }
51+
52+
public Task<BatchResult<ReportRecord>> ReadBatchAsync(BatchContext context, CancellationToken cancellationToken)
53+
{
54+
var skip = (context.PageNumber - 1) * context.PageSize;
55+
var page = _rows.Skip(skip).Take(context.PageSize).ToArray();
56+
var hasMore = skip + page.Length < _rows.Count;
57+
var nextCursor = hasMore
58+
? context.PageNumber.ToString(CultureInfo.InvariantCulture)
59+
: null;
60+
return Task.FromResult(new BatchResult<ReportRecord>(page, nextCursor, hasMore));
61+
}
62+
}
63+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using NeoReports.Abstractions;
4+
using NeoReports.Core.Configuration;
5+
using NeoReports.Core.Pipeline;
6+
using NeoReports.Destinations.Local;
7+
using NeoReports.Formats.Csv;
8+
using NeoReports.Samples.DynamicConfigCsv;
9+
10+
// Sample 04 — config-driven report (the dynamic path).
11+
//
12+
// The whole report is defined in report.json: there is NO typed POCO. Rows flow through the same
13+
// pipeline as the typed path as positional ReportRecords. What the JSON fully drives today: the
14+
// report name, the source selection (by id), the columns/schema (name, type, header, format,
15+
// culture), and the selection of outputs/destinations (by id).
16+
//
17+
// Standing in for things not built yet:
18+
// - the SQL config source arrives in A3, so an in-memory IConfigSourceProvider provides the rows;
19+
// - binding format/destination *options* from config arrives later (A5), so the CSV and Local
20+
// factories are pre-wired in DI (the JSON's output/destination "properties" are illustrative).
21+
//
22+
// dotnet run --project samples/04-dynamic-config-csv
23+
24+
var configPath = Path.Combine(AppContext.BaseDirectory, "report.json");
25+
var json = await File.ReadAllTextAsync(configPath);
26+
27+
// 1) Parse the JSON document into a ReportConfig.
28+
var config = new JsonReportConfigParser().Parse(json);
29+
30+
// 2) Register the providers/factories the compiler resolves by stable id.
31+
var services = new ServiceCollection();
32+
services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));
33+
services.AddSingleton<IConfigSourceProvider, InMemorySalesSourceProvider>(); // source "inmemory"
34+
services.AddSingleton<IWriterFactory>(new CsvWriterFactory(new CsvOptions())); // format "csv"
35+
services.AddSingleton<IDestinationFactory>(
36+
new LocalDestinationFactory("./out/{name}-{date:yyyy-MM-dd}.{ext}")); // dest "local"
37+
38+
await using var provider = services.BuildServiceProvider();
39+
40+
// 3) Compile the config into the same runnable report the fluent builder produces.
41+
var report = ReportConfigCompiler.Compile(config, provider);
42+
43+
// 4) Run it.
44+
var logger = provider.GetRequiredService<ILoggerFactory>().CreateLogger("dynamic");
45+
var exec = new ReportExecutionContext(
46+
Guid.NewGuid().ToString("N"), config.Name, parameters: null, logger, CancellationToken.None);
47+
48+
var result = await ReportRunner.ExecuteAsync(report, exec, provider, CancellationToken.None);
49+
50+
Console.WriteLine($"Report: {config.Name}");
51+
Console.WriteLine($"Status: {result.Status}");
52+
Console.WriteLine($"Records read/written: {result.Stats.RecordsRead}/{result.Stats.RecordsWritten}");
53+
foreach (var upload in result.Uploads)
54+
Console.WriteLine($"Uploaded: {upload.RemotePath} (success={upload.Success})");
55+
56+
return result.Status == ReportRunStatus.Failed ? 1 : 0;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# 04 — dynamic config → CSV (the dynamic path)
2+
3+
Defines a report entirely in [`report.json`](report.json)**no typed POCO**. The JSON is parsed
4+
into a `ReportConfig` and compiled into the same runnable report the fluent builder produces; rows
5+
flow through the existing pipeline as positional `ReportRecord`s.
6+
7+
```bash
8+
dotnet run --project samples/04-dynamic-config-csv
9+
# writes ./out/monthly-sales-<date>.csv
10+
```
11+
12+
What the JSON drives today:
13+
14+
- the report **name** and **page size**;
15+
- the **source** selection by id (`"type": "inmemory"`) and its properties (`rows`);
16+
- the **columns / schema**: name, semantic type, header, format and culture;
17+
- the **outputs** and **destinations** selection by id (`"csv"`, `"local"`).
18+
19+
Standing in for not-yet-built pieces:
20+
21+
- the **SQL** config source arrives in **A3**, so an in-memory `IConfigSourceProvider`
22+
([`InMemorySalesSourceProvider`](InMemorySalesSourceProvider.cs)) supplies the rows;
23+
- binding format/destination **options** from config arrives later (**A5**), so the CSV and Local
24+
factories are pre-wired in DI — the JSON's `properties` under `outputs`/`destinations` are
25+
illustrative for now.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "monthly-sales",
3+
"pageSize": 100,
4+
"source": {
5+
"type": "inmemory",
6+
"properties": { "rows": 5 }
7+
},
8+
"columns": [
9+
{ "name": "Id", "type": "Integer", "displayName": "Sale ID", "nullable": false },
10+
{ "name": "Customer", "type": "String" },
11+
{ "name": "Amount", "type": "Decimal", "displayName": "Amount", "format": "C2", "culture": "pt-BR" },
12+
{ "name": "Date", "type": "DateTime", "displayName": "Sale Date", "format": "yyyy-MM-dd" }
13+
],
14+
"outputs": [ { "format": "csv" } ],
15+
"destinations": [
16+
{ "type": "local", "properties": { "path": "./out/{name}-{date:yyyy-MM-dd}.{ext}" } }
17+
]
18+
}

0 commit comments

Comments
 (0)