Skip to content

Commit 5b9c9dd

Browse files
committed
chore(quality): materialize IEnumerable to avoid multi-enumeration
Fixes the 29 PossibleMultipleEnumeration findings flagged by ReSharper. Each call site enumerated an IEnumerable two-or-more times (e.g., `source.Any()` then `source.First()`, or `source.Sum(a)` then `source.Sum(b)`). Materialising once with `.ToList()` is semantically equivalent and avoids re-evaluating deferred queries. - Test files: cast assertion targets to lists so xUnit's `Assert.NotEmpty` + `Assert.Equal(N, x.Count())` doesn't iterate twice. - VueTableDefault.cs: `data`, `skipColumns`, `altColumnNames` are enumerated 3+ times per helper and 3 times across helpers from `InvokeAsync`. Materialise at both layers. - EvaluationPolicyService.IsRequiredFor*: `rotationWeeks` is enumerated by `.Any()`, `.FirstOrDefault(currentWeek)`, and `.FirstOrDefault(nextWeek)`. Materialise at top of method. - CMSContentController.GetContentBlockByFn: switch `Any()/First()` to `Count == 0` / indexer. - CliniciansController.FilterCliniciansByPermissions: rename param to `cliniciansSource`, materialise once into `clinicians`. - Smaller fixes in RotationsController, EvaluationReportService.
1 parent 38329f9 commit 5b9c9dd

8 files changed

Lines changed: 53 additions & 31 deletions

File tree

test/ClinicalScheduler/Integration/ControllerServiceIntegrationTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ public async Task CliniciansController_UsesPersonServiceExclusively_NoDirectAAUD
108108

109109
// Assert - Verify PersonService was used and returned DTOs
110110
var okResult = Assert.IsType<OkObjectResult>(result);
111-
var clinicians = Assert.IsAssignableFrom<IEnumerable<object>>(okResult.Value);
111+
var clinicians = Assert.IsAssignableFrom<IEnumerable<object>>(okResult.Value).ToList();
112112
Assert.NotEmpty(clinicians);
113113
// Verify the data came from PersonService by checking it matches our seed data
114-
Assert.Equal(2, clinicians.Count());
114+
Assert.Equal(2, clinicians.Count);
115115
}
116116

117117

@@ -316,7 +316,7 @@ public Task CompleteRequestFlow_WithDTOValidation_WorksEndToEnd()
316316
};
317317

318318
var validationContext = new ValidationContext(invalidRequest);
319-
var validationResults = invalidRequest.Validate(validationContext);
319+
var validationResults = invalidRequest.Validate(validationContext).ToList();
320320

321321
Assert.NotEmpty(validationResults);
322322
Assert.Contains(validationResults, v => v.ErrorMessage == "Week IDs must be unique");

test/Effort/CourseRelationshipsControllerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,9 @@ public async Task GetAvailableChildren_FiltersResultsByAuthorizedDepartments()
304304

305305
// Assert
306306
var okResult = Assert.IsType<OkObjectResult>(actionResult.Result);
307-
var returnedCourses = Assert.IsAssignableFrom<IEnumerable<CourseDto>>(okResult.Value);
307+
var returnedCourses = Assert.IsAssignableFrom<IEnumerable<CourseDto>>(okResult.Value).ToList();
308308
Assert.Single(returnedCourses);
309-
Assert.Equal(DvmDept, returnedCourses.First().CustDept);
309+
Assert.Equal(DvmDept, returnedCourses[0].CustDept);
310310
}
311311

312312
#endregion

test/Effort/InstructorsControllerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,9 +469,9 @@ public async Task GetDepartments_ReturnsOnlyAuthorizedDepartments_ForNonAdminUse
469469

470470
// Assert
471471
var okResult = Assert.IsType<OkObjectResult>(result.Result);
472-
var returnedDepartments = Assert.IsAssignableFrom<IEnumerable<DepartmentDto>>(okResult.Value);
472+
var returnedDepartments = Assert.IsAssignableFrom<IEnumerable<DepartmentDto>>(okResult.Value).ToList();
473473
Assert.Single(returnedDepartments);
474-
Assert.Equal("VME", returnedDepartments.First().Code);
474+
Assert.Equal("VME", returnedDepartments[0].Code);
475475
}
476476

477477
#endregion

web/Areas/CMS/Controllers/CMSContentController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ public ActionResult<List<ContentBlock>> GetContentBlocks()
4242
[HttpGet("fn/{friendlyName}")]
4343
public ActionResult<ContentBlock?> GetContentBlockByFn(string friendlyName)
4444
{
45-
var blocks = new Data.CMS(_context, _rapsContext, _sanitizerService).GetContentBlocksAllowed(null, friendlyName, null, null, null, null, null, null);
46-
if (blocks == null || !blocks.Any())
45+
var blocks = new Data.CMS(_context, _rapsContext, _sanitizerService).GetContentBlocksAllowed(null, friendlyName, null, null, null, null, null, null)?.ToList();
46+
if (blocks == null || blocks.Count == 0)
4747
{
4848
return NotFound();
4949
}
5050

51-
return blocks.First();
51+
return blocks[0];
5252
}
5353

5454
//PUT: content/5

web/Areas/ClinicalScheduler/Controllers/CliniciansController.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,9 @@ private static object CreateDefaultClinicianInfo(string mothraId, string? role =
533533
/// </summary>
534534
/// <param name="clinicians">List of all available clinicians</param>
535535
/// <returns>Filtered list of clinicians based on user permissions</returns>
536-
private IEnumerable<ClinicianSummary> FilterCliniciansByPermissions(IEnumerable<ClinicianSummary> clinicians, string? viewContext = null)
536+
private List<ClinicianSummary> FilterCliniciansByPermissions(IEnumerable<ClinicianSummary> cliniciansSource, string? viewContext = null)
537537
{
538+
var clinicians = cliniciansSource.ToList();
538539
try
539540
{
540541
var currentUser = _userHelper.GetCurrentUser();

web/Areas/ClinicalScheduler/Controllers/RotationsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public async Task<ActionResult<object>> GetRotationSchedule(int id, [FromQuery]
219219

220220
// Get person data for all involved clinicians
221221
var uniqueMothraIds = baseInstructorSchedules.Select(i => i.MothraId).Distinct();
222-
var recentClinicianMothraIds = recentCliniciansData.Select(c => c.MothraId).Distinct();
222+
var recentClinicianMothraIds = recentCliniciansData.Select(c => c.MothraId).Distinct().ToList();
223223
var allMothraIds = uniqueMothraIds.Concat(recentClinicianMothraIds);
224224
var personData = await GetPersonDataBatchAsync(allMothraIds);
225225

web/Areas/ClinicalScheduler/Services/EvaluationPolicyService.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,18 @@ public bool RequiresPrimaryEvaluator(
4242
}
4343

4444
// Validate input data
45-
if (rotationWeeks == null || !rotationWeeks.Any())
45+
if (rotationWeeks == null)
46+
{
47+
return false;
48+
}
49+
var weeks = rotationWeeks.ToList();
50+
if (weeks.Count == 0)
4651
{
4752
return false;
4853
}
4954

5055
// Find the current week in the rotation schedule
51-
var currentWeek = rotationWeeks.FirstOrDefault(w => w.WeekNum == weekNumber);
56+
var currentWeek = weeks.FirstOrDefault(w => w.WeekNum == weekNumber);
5257
if (currentWeek == null)
5358
{
5459
return false; // Week not found in rotation schedule
@@ -82,7 +87,7 @@ public bool RequiresPrimaryEvaluator(
8287
// 2. Either the next week is a StartWeek OR there is no next week
8388
if (!currentWeek.StartWeek)
8489
{
85-
var nextWeek = rotationWeeks.FirstOrDefault(w => w.WeekNum == weekNumber + 1);
90+
var nextWeek = weeks.FirstOrDefault(w => w.WeekNum == weekNumber + 1);
8691

8792
// Special case: If next week is extended, this week doesn't need evaluation
8893
// Extended weeks are continuation weeks that don't count for evaluation

web/Views/Shared/Components/VueTableDefault/VueTableDefault.cs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ public async Task<IViewComponentResult> InvokeAsync(IEnumerable<Object>? data, s
1717
IEnumerable<string>? skipColumnsVisible = null
1818
)
1919
{
20+
// Materialise once: each helper enumerates these collections multiple times.
21+
var dataList = data?.ToList();
22+
var skipList = skipColumns?.ToList();
23+
var altList = altColumnNames?.ToList();
24+
var skipVisibleList = skipColumnsVisible?.ToList();
25+
2026
ViewData["UniquePrepend"] = User?.Identity?.Name + DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
2127
ViewData["KeyColumnName"] = keyColumnName;
22-
ViewData["Columns"] = GetDefaultColumnNames(data, skipColumns, altColumnNames);
23-
ViewData["Rows"] = GetDefaultRows(data, skipColumns);
24-
ViewData["VisibleColumns"] = GetDefaultVisibleColumns(data, skipColumnsVisible);
28+
ViewData["Columns"] = GetDefaultColumnNames(dataList, skipList, altList);
29+
ViewData["Rows"] = GetDefaultRows(dataList, skipList);
30+
ViewData["VisibleColumns"] = GetDefaultVisibleColumns(dataList, skipVisibleList);
2531

2632
return await Task.Run(() => View("Default"));
2733
}
@@ -36,16 +42,20 @@ public static string GetDefaultColumnNames(IEnumerable<Object>? data, IEnumerabl
3642
{
3743
StringBuilder output = new StringBuilder();
3844

39-
if (data != null)
45+
var dataList = data?.ToList();
46+
var skipList = skipColumns?.ToList();
47+
var altList = altColumnNames?.ToList();
48+
49+
if (dataList != null)
4050
{
4151
output.Append('[');
4252

4353
var includedProperties =
44-
(from property in data.First().GetType().GetProperties()
54+
(from property in dataList[0].GetType().GetProperties()
4555
where property.PropertyType == typeof(string)
4656
|| !typeof(IEnumerable).IsAssignableFrom(property.PropertyType) // skip ICollection columns
4757
select property)
48-
.Where(p => skipColumns == null || !skipColumns.Contains(p.Name))
58+
.Where(p => skipList == null || !skipList.Contains(p.Name))
4959
.ToList();
5060

5161
var last = includedProperties.LastOrDefault();
@@ -60,13 +70,13 @@ public static string GetDefaultColumnNames(IEnumerable<Object>? data, IEnumerabl
6070
output.Append("sortable:true,");
6171
output.Append(String.Format("field:'{0}',", propertyName));
6272

63-
if (altColumnNames == null)
73+
if (altList == null)
6474
{
6575
output.Append(String.Format("label:'{0}'", propertyName));
6676
}
6777
else
6878
{
69-
string? altName = altColumnNames.FirstOrDefault(alt => alt.Item1.ToLower().Equals(propertyName.ToLower()))?.Item2;
79+
string? altName = altList.FirstOrDefault(alt => alt.Item1.ToLower().Equals(propertyName.ToLower()))?.Item2;
7080

7181
if (altName != null)
7282
{
@@ -105,23 +115,26 @@ public static string GetDefaultRows(IEnumerable<Object>? data, IEnumerable<strin
105115
{
106116
StringBuilder output = new StringBuilder();
107117

108-
if (data != null)
118+
var dataList = data?.ToList();
119+
var skipList = skipColumns?.ToList();
120+
121+
if (dataList != null)
109122
{
110123
var includedProperties =
111-
(from property in data.First().GetType().GetProperties()
124+
(from property in dataList[0].GetType().GetProperties()
112125
where property.PropertyType == typeof(string)
113126
|| !typeof(IEnumerable).IsAssignableFrom(property.PropertyType) // skip ICollection columns
114127
select property)
115-
.Where(p => skipColumns == null || !skipColumns.Contains(p.Name))
128+
.Where(p => skipList == null || !skipList.Contains(p.Name))
116129
.ToList();
117130

118131
var last = includedProperties.LastOrDefault();
119132

120133
output.Append('[');
121134

122-
var lastObj = data.Last();
135+
var lastObj = dataList[^1];
123136

124-
foreach (var obj in data)
137+
foreach (var obj in dataList)
125138
{
126139
output.Append('{');
127140

@@ -172,16 +185,19 @@ public static string GetDefaultVisibleColumns(IEnumerable<Object>? data, IEnumer
172185
{
173186
StringBuilder output = new StringBuilder();
174187

175-
if (data != null)
188+
var dataList = data?.ToList();
189+
var skipList = skipColumns?.ToList();
190+
191+
if (dataList != null)
176192
{
177193
output.Append('[');
178194

179195
var includedProperties =
180-
(from property in data.First().GetType().GetProperties()
196+
(from property in dataList[0].GetType().GetProperties()
181197
where property.PropertyType == typeof(string)
182198
|| !typeof(IEnumerable).IsAssignableFrom(property.PropertyType) // skip ICollection columns
183199
select property)
184-
.Where(p => skipColumns == null || !skipColumns.Contains(p.Name))
200+
.Where(p => skipList == null || !skipList.Contains(p.Name))
185201
.ToList();
186202

187203
var last = includedProperties.LastOrDefault();

0 commit comments

Comments
 (0)