@@ -14,9 +14,43 @@ public async ValueTask<AuditSummaryAggregateDto> Handle(GetAuditSummaryQuery que
1414 ArgumentNullException . ThrowIfNull ( query ) ;
1515
1616 var audits = ApplyFilters ( dbContext . AuditRecords . AsNoTracking ( ) , query ) ;
17- var list = await audits . ToListAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
1817
19- return AggregateRecords ( list ) ;
18+ // Push all aggregation to the database via GROUP BY — avoids loading any rows into memory.
19+ // EF Core DbContext is not thread-safe, so queries are run sequentially.
20+
21+ var byType = await audits
22+ . GroupBy ( a => a . EventType )
23+ . Select ( g => new { EventType = g . Key , Count = g . LongCount ( ) } )
24+ . ToDictionaryAsync ( g => ( AuditEventType ) g . EventType , g => g . Count , cancellationToken )
25+ . ConfigureAwait ( false ) ;
26+
27+ var bySeverity = await audits
28+ . GroupBy ( a => a . Severity )
29+ . Select ( g => new { Severity = g . Key , Count = g . LongCount ( ) } )
30+ . ToDictionaryAsync ( g => ( AuditSeverity ) g . Severity , g => g . Count , cancellationToken )
31+ . ConfigureAwait ( false ) ;
32+
33+ var bySource = await audits
34+ . Where ( a => a . Source != null )
35+ . GroupBy ( a => a . Source ! )
36+ . Select ( g => new { Source = g . Key , Count = g . LongCount ( ) } )
37+ . ToDictionaryAsync ( g => g . Source , g => g . Count , StringComparer . OrdinalIgnoreCase , cancellationToken )
38+ . ConfigureAwait ( false ) ;
39+
40+ var byTenant = await audits
41+ . Where ( a => a . TenantId != null )
42+ . GroupBy ( a => a . TenantId ! )
43+ . Select ( g => new { TenantId = g . Key , Count = g . LongCount ( ) } )
44+ . ToDictionaryAsync ( g => g . TenantId , g => g . Count , StringComparer . OrdinalIgnoreCase , cancellationToken )
45+ . ConfigureAwait ( false ) ;
46+
47+ return new AuditSummaryAggregateDto
48+ {
49+ EventsByType = byType ,
50+ EventsBySeverity = bySeverity ,
51+ EventsBySource = bySource ,
52+ EventsByTenant = byTenant
53+ } ;
2054 }
2155
2256 private static IQueryable < AuditRecord > ApplyFilters ( IQueryable < AuditRecord > audits , GetAuditSummaryQuery query )
@@ -38,47 +72,4 @@ private static IQueryable<AuditRecord> ApplyFilters(IQueryable<AuditRecord> audi
3872
3973 return audits ;
4074 }
41-
42- private static AuditSummaryAggregateDto AggregateRecords ( List < AuditRecord > records )
43- {
44- var aggregate = new AuditSummaryAggregateDto ( ) ;
45-
46- foreach ( var record in records )
47- {
48- AggregateByType ( aggregate , record ) ;
49- AggregrateBySeverity ( aggregate , record ) ;
50- AggregateBySource ( aggregate , record ) ;
51- AggregateByTenant ( aggregate , record ) ;
52- }
53-
54- return aggregate ;
55- }
56-
57- private static void AggregateByType ( AuditSummaryAggregateDto aggregate , AuditRecord record )
58- {
59- var type = ( AuditEventType ) record . EventType ;
60- aggregate . EventsByType [ type ] = aggregate . EventsByType . TryGetValue ( type , out var c ) ? c + 1 : 1 ;
61- }
62-
63- private static void AggregrateBySeverity ( AuditSummaryAggregateDto aggregate , AuditRecord record )
64- {
65- var severity = ( AuditSeverity ) record . Severity ;
66- aggregate . EventsBySeverity [ severity ] = aggregate . EventsBySeverity . TryGetValue ( severity , out var s ) ? s + 1 : 1 ;
67- }
68-
69- private static void AggregateBySource ( AuditSummaryAggregateDto aggregate , AuditRecord record )
70- {
71- if ( ! string . IsNullOrWhiteSpace ( record . Source ) )
72- {
73- aggregate . EventsBySource [ record . Source ] = aggregate . EventsBySource . TryGetValue ( record . Source , out var cs ) ? cs + 1 : 1 ;
74- }
75- }
76-
77- private static void AggregateByTenant ( AuditSummaryAggregateDto aggregate , AuditRecord record )
78- {
79- if ( ! string . IsNullOrWhiteSpace ( record . TenantId ) )
80- {
81- aggregate . EventsByTenant [ record . TenantId ] = aggregate . EventsByTenant . TryGetValue ( record . TenantId , out var ct ) ? ct + 1 : 1 ;
82- }
83- }
84- }
75+ }
0 commit comments