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());
        }