private static IReadOnlyCollection <RowMatchingResult> ListDataMismatches <TRowType>( TableRows expectedRows, IEnumerable <TRowType> actualRows, Func <TRowType, string, object> getCellValue) { var firstColumnName = expectedRows.First().Keys.First(); var expectedRowsWithIndexAndKey = expectedRows.Select((row, index) => new { row, index, key = row[0] }).AsImmutable(); var actualRowsWithKey = actualRows.Select((row, index) => new { row, index, key = getCellValue(row, firstColumnName).ToString() }).AsImmutable(); var mismatchedAndMissingRows = from expectedRow in expectedRowsWithIndexAndKey join actualRow in actualRowsWithKey on expectedRow.key equals actualRow.key into matchedActualRows from matchedActualRow in matchedActualRows.DefaultIfEmpty() let rowMatchingResult = matchedActualRow != null ? MatchRows(expectedRow.row, matchedActualRow.row, expectedRow.index, getCellValue) : new MissingRow(expectedRow.key, expectedRow.index) where rowMatchingResult != null select rowMatchingResult; var unnecessaryRows = from actualRow in actualRowsWithKey where expectedRowsWithIndexAndKey.All(row => row.key != actualRow.key) select new UnnecessaryRow(actualRow.key); return(mismatchedAndMissingRows.Concat(unnecessaryRows).AsImmutable()); }