@@ -27,21 +27,28 @@ public class Transformer<T>
2727 /// <summary>
2828 /// The type of <typeparamref name="T"/>.
2929 /// </summary>
30- public Type Type { get ; }
30+ public Type Type => typeof ( T ) ;
3131
32- private readonly PropertyInfo [ ] _properties ;
32+ // Reflection is invariant per T; a static on this generic type is per-T and initialized once
33+ // (thread-safe), so GetProperties() and the name->PropertyInfo map aren't rebuilt per query.
34+ // Built once, read on every query (property lookups + Keys) -> frozen on the modern target.
35+ [ SuppressMessage ( "Roslynator" , "RCS1158:Static member in generic type should use a type parameter." , Justification = "Per-T reflection cache is intentional." ) ]
36+ #if NET10_0_OR_GREATER
37+ static readonly FrozenDictionary < string , PropertyInfo > PropertiesByName
38+ = typeof ( T ) . GetProperties ( ) . ToFrozenDictionary ( p => p . Name ) ;
39+ #else
40+ static readonly Dictionary < string , PropertyInfo > PropertiesByName
41+ = typeof ( T ) . GetProperties ( ) . ToDictionary ( p => p . Name ) ;
42+ #endif
3343
3444 // Allow mapping key = object property, value = column name.
3545 readonly Dictionary < string , string > _propertyMap ;
3646
3747 // Column-name -> property lookup, matched case-insensitively (OrdinalIgnoreCase) so no
38- // upper-cased key strings are allocated. Built once and never mutated after construction,
39- // so it is frozen on the modern target for faster reads.
40- #if NET10_0_OR_GREATER
41- readonly FrozenDictionary < string , PropertyInfo > _columnToPropertyMap ;
42- #else
48+ // upper-cased key strings are allocated. This map is per-query (a fresh Transformer is built
49+ // per operation) and read only a handful of times, so a plain Dictionary is correct here —
50+ // freezing a short-lived map costs more to build than the few lookups would ever recover.
4351 readonly Dictionary < string , PropertyInfo > _columnToPropertyMap ;
44- #endif
4552
4653 /// <summary>
4754 /// The property names.
@@ -58,31 +65,25 @@ public class Transformer<T>
5865 /// </summary>
5966 protected internal Transformer ( IEnumerable < ( string Field , string ? Column ) > ? overrides = null )
6067 {
61- Type = typeof ( T ) ;
62- _properties = Type . GetProperties ( ) ;
63- _propertyMap = _properties . Select ( p => p . Name ) . ToDictionary ( n => n ) ;
64-
65- Dictionary < string , PropertyInfo > pm = _properties . ToDictionary ( p => p . Name ) ;
68+ // Per-instance copy of the default (property-name -> same column-name) map, sized exactly,
69+ // so the optional overrides can mutate it without touching the shared reflection cache.
70+ _propertyMap = new Dictionary < string , string > ( PropertiesByName . Count ) ;
71+ foreach ( string name in PropertiesByName . Keys )
72+ _propertyMap [ name ] = name ;
6673
6774 if ( overrides != null )
6875 {
6976 foreach ( ( string Field , string ? Column ) in overrides )
7077 {
71- string ? cn = Column ;
72- if ( cn == null ) _propertyMap . Remove ( Field ) ; // Null values indicate a desire to 'ignore' a field.
73- else _propertyMap [ Field ] = cn ;
78+ if ( Column == null ) _propertyMap . Remove ( Field ) ; // Null column indicates 'ignore this field'.
79+ else _propertyMap [ Field ] = Column ;
7480 }
7581 }
7682
77- // Project column-name -> property straight into the target map with a case-insensitive
78- // comparer: no upper-cased key strings, and no intermediate Dictionary on the frozen path.
79- #if NET10_0_OR_GREATER
80- _columnToPropertyMap = _propertyMap . ToFrozenDictionary (
81- kvp => kvp . Value , kvp => pm [ kvp . Key ] , StringComparer . OrdinalIgnoreCase ) ;
82- #else
83+ // Project column-name -> property directly with a case-insensitive comparer (no upper-cased
84+ // key strings). A plain Dictionary — this map lives only for the duration of one query.
8385 _columnToPropertyMap = _propertyMap . ToDictionary (
84- kvp => kvp . Value , kvp => pm [ kvp . Key ] , StringComparer . OrdinalIgnoreCase ) ;
85- #endif
86+ kvp => kvp . Value , kvp => PropertiesByName [ kvp . Key ] , StringComparer . OrdinalIgnoreCase ) ;
8687 }
8788
8889 /// <summary>
@@ -207,6 +208,24 @@ public IEnumerable<T> AsDequeueingEnumerable(QueryResult<Queue<object[]>> result
207208 } ) ;
208209 }
209210
211+ // Splits the matched (Name, Ordinal) columns into an ordinal array (which hits the IList<int>
212+ // fast path in the reader enumeration) and an exactly-sized immutable name array, in one pass —
213+ // avoiding two lazy Select iterators and a growing ImmutableArray builder per query.
214+ static ( int [ ] Ordinals , ImmutableArray < string > Names ) SplitColumns ( ( string Name , int Ordinal ) [ ] columns )
215+ {
216+ int n = columns . Length ;
217+ var ordinals = new int [ n ] ;
218+ var names = ImmutableArray . CreateBuilder < string > ( n ) ;
219+ names . Count = n ;
220+ for ( int i = 0 ; i < n ; i ++ )
221+ {
222+ ordinals [ i ] = columns [ i ] . Ordinal ;
223+ names [ i ] = columns [ i ] . Name ;
224+ }
225+
226+ return ( ordinals , names . MoveToImmutable ( ) ) ;
227+ }
228+
210229 /// <inheritdoc cref="ResultsBuffered(IDataReader, bool)"/>
211230 internal IEnumerable < T > Results ( IDataReader reader )
212231 {
@@ -215,11 +234,12 @@ internal IEnumerable<T> Results(IDataReader reader)
215234
216235 // Ignore missing columns.
217236 ( string Name , int Ordinal ) [ ] columns = reader . GetMatchingOrdinals ( _propertyMap . Values , true ) ;
218- var processor = new Processor ( this , columns . Select ( m => m . Name ) . ToImmutableArray ( ) ) ;
237+ ( int [ ] ordinals , ImmutableArray < string > names ) = SplitColumns ( columns ) ;
238+ var processor = new Processor ( this , names ) ;
219239 Func < object ? [ ] , T > transform = processor . Transform ;
220240
221241 return reader
222- . AsEnumerable ( columns . Select ( m => m . Ordinal ) , LocalPool )
242+ . AsEnumerable ( ordinals , LocalPool )
223243 . Select ( a =>
224244 {
225245 try
@@ -250,6 +270,9 @@ internal IEnumerable<T> ResultsBuffered(IDataReader reader, bool readStarted)
250270 // Ignore missing columns.
251271 ( string Name , int Ordinal ) [ ] columns = reader . GetMatchingOrdinals ( _propertyMap . Values , true ) ;
252272
273+ // NOTE: pass the lazy projections straight through — RetrieveInternal materializes the
274+ // ordinals into an ImmutableArray that QueryResult then reuses zero-copy (Immute fast path);
275+ // pre-materializing here would add a redundant array.
253276 return AsDequeueingEnumerable (
254277 CoreExtensions . RetrieveInternal (
255278 LocalPool ,
@@ -275,9 +298,10 @@ async IAsyncEnumerable<T> ResultsAsyncCore(DbDataReader reader, [EnumeratorCance
275298 {
276299 // Ignore missing columns.
277300 ( string Name , int Ordinal ) [ ] columns = reader . GetMatchingOrdinals ( _propertyMap . Values , true ) ;
278- var processor = new Processor ( this , columns . Select ( m => m . Name ) . ToImmutableArray ( ) ) ;
301+ ( int [ ] ordinals , ImmutableArray < string > names ) = SplitColumns ( columns ) ;
302+ var processor = new Processor ( this , names ) ;
279303
280- await foreach ( object [ ] a in reader . AsAsyncEnumerable ( columns . Select ( m => m . Ordinal ) , LocalPool , cancellationToken ) )
304+ await foreach ( object [ ] a in reader . AsAsyncEnumerable ( ordinals , LocalPool , cancellationToken ) )
281305 {
282306 try
283307 {
@@ -303,17 +327,30 @@ public IEnumerable<T> Results(DataTable table, bool clearTable)
303327 if ( table is null ) throw new ArgumentNullException ( nameof ( table ) ) ;
304328 Contract . EndContractBlock ( ) ;
305329
306- int columnCount = table . Columns . Count ;
307- IEnumerable < DataColumn > columns = table . Columns . AsEnumerable ( ) ;
308- var results = new QueryResult < Queue < object [ ] > > (
309- columns . Select ( c => c . Ordinal ) ,
310- columns . Select ( c => c . ColumnName ) ,
311- new Queue < object [ ] > ( table . Rows . AsEnumerable ( ) . Select ( r =>
312- {
313- object [ ] a = LocalPool . Rent ( columnCount ) ;
314- for ( int i = 0 ; i < columnCount ; i ++ ) a [ i ] = r [ i ] ;
315- return a ;
316- } ) ) ) ;
330+ DataColumnCollection cols = table . Columns ;
331+ int columnCount = cols . Count ;
332+
333+ // Ordinals + names in a single pass (no double Select iterators).
334+ var ordinals = new int [ columnCount ] ;
335+ var names = new string [ columnCount ] ;
336+ for ( int i = 0 ; i < columnCount ; i ++ )
337+ {
338+ DataColumn c = cols [ i ] ;
339+ ordinals [ i ] = c . Ordinal ;
340+ names [ i ] = c . ColumnName ;
341+ }
342+
343+ // The row count is known, so pre-size the queue and fill it directly.
344+ DataRowCollection rows = table . Rows ;
345+ var buffer = new Queue < object [ ] > ( rows . Count ) ;
346+ foreach ( DataRow r in rows )
347+ {
348+ object [ ] a = LocalPool . Rent ( columnCount ) ;
349+ for ( int i = 0 ; i < columnCount ; i ++ ) a [ i ] = r [ i ] ;
350+ buffer . Enqueue ( a ) ;
351+ }
352+
353+ var results = new QueryResult < Queue < object [ ] > > ( ordinals , names , buffer ) ;
317354
318355 if ( clearTable ) table . Rows . Clear ( ) ;
319356 return AsDequeueingEnumerable ( results , LocalPool ) ;
0 commit comments