11using System . Linq . Expressions ;
22using NeoReports . Abstractions ;
33using NeoReports . Core . Pipeline ;
4+ using NeoReports . Core . Sections ;
45using NeoReports . Core . Sources ;
56
67namespace NeoReports . Core . Building ;
@@ -18,6 +19,7 @@ public sealed class ReportBuilder<TRow>
1819 private readonly List < Func < TRow , bool > > _filters = new ( ) ;
1920 private readonly List < ColumnDefinition < TRow > > _columns = new ( ) ;
2021 private readonly List < OutputEntry > _outputs = new ( ) ;
22+ private readonly List < SectionedEntry > _sectioned = [ ] ;
2123 private readonly List < DestinationSpec > _destinations = new ( ) ;
2224 private readonly RetryOptions _retry = new ( ) ;
2325 private readonly FailureStrategyBuilder _failure = new ( ) ;
@@ -153,6 +155,28 @@ public ReportBuilder<TRow> To(OutputSpec output, Action<OutputView<TRow>> config
153155 return this ;
154156 }
155157
158+ /// <summary>
159+ /// Adds a sectioned output: a single file with several named sections (e.g. an XLSX workbook with
160+ /// one worksheet per section), each with its own filters and/or columns, all produced from one
161+ /// source read.
162+ /// </summary>
163+ /// <param name="output">The sectioned output specification (from a format package).</param>
164+ /// <param name="configureSections">Declares the sections (name + view).</param>
165+ /// <exception cref="ConfigurationException">Thrown when no sections are declared.</exception>
166+ public ReportBuilder < TRow > ToSections ( SectionedOutputSpec output , Action < SectionBuilder < TRow > > configureSections )
167+ {
168+ ArgumentNullException . ThrowIfNull ( output ) ;
169+ ArgumentNullException . ThrowIfNull ( configureSections ) ;
170+
171+ var sections = new SectionBuilder < TRow > ( ) ;
172+ configureSections ( sections ) ;
173+ if ( sections . Sections . Count == 0 )
174+ throw new ConfigurationException ( $ "Report '{ _name } ' has a sectioned output with no sections. Call Section(...).") ;
175+
176+ _sectioned . Add ( new SectionedEntry ( output , sections . Sections ) ) ;
177+ return this ;
178+ }
179+
156180 /// <summary>Adds a destination the finished files are uploaded to.</summary>
157181 /// <param name="destination">The destination specification (e.g. from a destination package).</param>
158182 public ReportBuilder < TRow > UploadTo ( DestinationSpec destination )
@@ -187,40 +211,50 @@ public CompiledReport Build()
187211 if ( _batchSource is null && _streamingSource is null )
188212 throw new ConfigurationException ( $ "Report '{ _name } ' has no source. Call From(...).") ;
189213
190- if ( _columns . Count == 0 && ! _outputs . Any ( o => o . View is { ViewColumns . Count : > 0 } ) )
191- throw new ConfigurationException ( $ "Report '{ _name } ' has no columns. Call Columns(...) or give an output view its own columns.") ;
214+ bool anyViewColumns =
215+ _outputs . Any ( o => o . View is { ViewColumns . Count : > 0 } ) ||
216+ _sectioned . Any ( s => s . Sections . Any ( sd => sd . View . ViewColumns . Count > 0 ) ) ;
217+ if ( _columns . Count == 0 && ! anyViewColumns )
218+ throw new ConfigurationException ( $ "Report '{ _name } ' has no columns. Call Columns(...) or give a view its own columns.") ;
192219
193220 var reportSchema = new ReportSchema ( _columns . Select ( c => c . Column ) . ToList ( ) ) ;
194221
195222 var outputSpecs = new OutputSpec [ _outputs . Count ] ;
196223 var outputSchemas = new ReportSchema [ _outputs . Count ] ;
197224 var projections = new OutputProjection < TRow > [ _outputs . Count ] ;
198-
199225 for ( var i = 0 ; i < _outputs . Count ; i ++ )
200226 {
201- OutputEntry entry = _outputs [ i ] ;
202- List < ColumnDefinition < TRow > > columns = entry . View is { ViewColumns . Count : > 0 } ? entry . View . ViewColumns : _columns ;
203- if ( columns . Count == 0 )
227+ ( ReportSchema schema , OutputProjection < TRow > projection ) = ResolveView ( _outputs [ i ] . View , $ "output #{ i + 1 } ") ;
228+ outputSpecs [ i ] = _outputs [ i ] . Spec ;
229+ outputSchemas [ i ] = schema ;
230+ projections [ i ] = projection ;
231+ }
232+
233+ var sectionedOutputs = new CompiledSectionedOutput [ _sectioned . Count ] ;
234+ var sectionedProjections = new IReadOnlyList < OutputProjection < TRow > > [ _sectioned . Count ] ;
235+ for ( var s = 0 ; s < _sectioned . Count ; s ++ )
236+ {
237+ SectionedEntry entry = _sectioned [ s ] ;
238+ var sectionMetas = new ReportSection [ entry . Sections . Count ] ;
239+ var sectionProjections = new OutputProjection < TRow > [ entry . Sections . Count ] ;
240+ for ( var sec = 0 ; sec < entry . Sections . Count ; sec ++ )
204241 {
205- throw new ConfigurationException (
206- $ "Report '{ _name } ' output #{ i + 1 } has no columns. Add report Columns(...) or give the view its own columns.") ;
242+ SectionDefinition < TRow > def = entry . Sections [ sec ] ;
243+ ( ReportSchema schema , OutputProjection < TRow > projection ) = ResolveView ( def . View , $ "section '{ def . Name } '") ;
244+ sectionMetas [ sec ] = new ReportSection ( def . Name , schema ) ;
245+ sectionProjections [ sec ] = projection ;
207246 }
208247
209- Func < TRow , bool > [ ] filters = entry . View is null || entry . View . ViewFilters . Count == 0
210- ? _filters . ToArray ( )
211- : _filters . Concat ( entry . View . ViewFilters ) . ToArray ( ) ;
212-
213- outputSpecs [ i ] = entry . Spec ;
214- outputSchemas [ i ] = new ReportSchema ( columns . Select ( c => c . Column ) . ToList ( ) ) ;
215- projections [ i ] = new OutputProjection < TRow > ( filters , columns . Select ( c => c . Getter ) . ToArray ( ) ) ;
248+ sectionedOutputs [ s ] = new CompiledSectionedOutput ( entry . Spec , sectionMetas ) ;
249+ sectionedProjections [ s ] = sectionProjections ;
216250 }
217251
218- var batchSource = _batchSource ;
219- var streamingSource = _streamingSource ;
220- var pageSize = _pageSize ;
252+ IBatchSource < TRow > ? batchSource = _batchSource ;
253+ IStreamingSource < TRow > ? streamingSource = _streamingSource ;
254+ int pageSize = _pageSize ;
221255
222256 IProjectedBatchReader ReaderFactory ( ReportExecutionContext execution ) =>
223- new TypedBatchReader < TRow > ( batchSource , streamingSource , execution , pageSize , projections ) ;
257+ new TypedBatchReader < TRow > ( batchSource , streamingSource , execution , pageSize , projections , sectionedProjections ) ;
224258
225259 return new CompiledReport (
226260 _name ,
@@ -229,11 +263,33 @@ IProjectedBatchReader ReaderFactory(ReportExecutionContext execution) =>
229263 ReaderFactory ,
230264 outputSpecs ,
231265 outputSchemas ,
266+ sectionedOutputs ,
232267 _destinations . ToArray ( ) ,
233268 _retry ,
234269 _failure . Build ( ) ) ;
235270 }
236271
272+ private ( ReportSchema Schema , OutputProjection < TRow > Projection ) ResolveView ( OutputView < TRow > ? view , string what )
273+ {
274+ List < ColumnDefinition < TRow > > columns = view is { ViewColumns . Count : > 0 } ? view . ViewColumns : _columns ;
275+ if ( columns . Count == 0 )
276+ {
277+ throw new ConfigurationException (
278+ $ "Report '{ _name } ' { what } has no columns. Add report Columns(...) or give it its own columns.") ;
279+ }
280+
281+ Func < TRow , bool > [ ] filters = view is null || view . ViewFilters . Count == 0
282+ ? _filters . ToArray ( )
283+ : _filters . Concat ( view . ViewFilters ) . ToArray ( ) ;
284+
285+ return (
286+ new ReportSchema ( columns . Select ( c => c . Column ) . ToList ( ) ) ,
287+ new OutputProjection < TRow > ( filters , columns . Select ( c => c . Getter ) . ToArray ( ) ) ) ;
288+ }
289+
237290 /// <summary>An output plus its optional per-output view (own filters/columns).</summary>
238291 private sealed record OutputEntry ( OutputSpec Spec , OutputView < TRow > ? View ) ;
292+
293+ /// <summary>A sectioned output plus its section definitions.</summary>
294+ private sealed record SectionedEntry ( SectionedOutputSpec Spec , IReadOnlyList < SectionDefinition < TRow > > Sections ) ;
239295}
0 commit comments