CsvHelperのヘッダー読み込みについて

# はじめに
[以前の記事](https://qiita.com/mkuwan/items/85efd866aa1d66c92d15)でCsvHelperの使い方の変更点についてお伝えしましたが、2024年3月時点で新しくアプリケーションを作成していて、改めてCsvHelperを使ってみたところ、ヘッダーの読み込みについて気づいたことがあったので、備忘録ついでに記事とさせていただきます。

ちなみにCsvHelperはバージョンアップするたびにいろいろと仕様の変更があり、以前の使い方と違っているなんてことがあります。
今回記事で紹介しているバージョンは31.0.2です。
いろいろとWeb記事などを見ても、うまくいかないとか、書き方が古いといったことがあるので迷う人も多いのではないかと思っています。この記事がその一助となれば幸いです。

# CsvHelperのヘッダー読み込み(間違い版)
著者データとしてサンプルを作成しています

- CSVデータ(文字コードはShift JISとします)
```text
"著者コード","著者名","著者名カナ"
1,"高橋","たかはし"
2,"鈴木","すずき"
3,"佐藤","さとう"
4,"田中","たなか"
5,"佐々木","ささき"
```

- DTO
```C#
class AuthorCsvDto
{
    [Index(0)]
    public int AuthorCode { get; set; }

    [Index(1)]
    public string AuthorName { get; set; }

    [Index(2)]
    public string AuthorNameKana { get; set; }
}
```
- CSV読み込みメソッド(いろいろとWeb記事を参考にした結果)
**注意) 実はこれだとヘッダーを必ず読み込んでしまいます！**
```C#
public static IEnumerable<T> ReadCsv<T>(string filePath, bool hasHeader, string encode = "utf-8")
{
    var encodingProvider = System.Text.CodePagesEncodingProvider.Instance;

    Encoding? encoding = encode switch
    {
        "utf-8" => Encoding.UTF8,
        "shift-jis" => encodingProvider.GetEncoding(932), // 932 S-JIS
        _ => Encoding.UTF8
    };

    Console.WriteLine("CsvHelperの読み込み");
    using var reader = new StreamReader(filePath, encoding!);
    using var csv = new CsvReader(reader, CultureInfo.CurrentCulture);
    
    //headerを読み込むかどうか
    if (hasHeader)
    {
        csv.Read();
        csv.ReadHeader();
    }

    var records = csv.GetRecords<T>();

    // デバッグ用に読み込んだ結果をコンソールに出力
    var returnValues = records.ToList();
    foreach (var record in returnValues)
    {
        var message = new StringBuilder();
        message.Append("CsvHelperで読み込んだ結果:");
        foreach (var property in typeof(T).GetProperties())
        {
            message.Append($"{ property.Name}:{property.GetValue(record)}");
        }
        Console.WriteLine(message);
    }

    return returnValues;
}
```
ヘッダーを読み込む場合は、csv.Read()を先に行ってから、csv.ReadHeader()を実行しましょうという説明の記事やら投稿をよく見ました。
それで自分もそのように実装し読み込んだ結果は以下の通りです(学習用Webアプリ画面です)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/a137361f-39de-7066-1eb2-a5ba2ec5e9a1.png)


ところが、ヘッダー無しの場合(header=false)どうなるか確認したところ結果は想像とは異なりました。
### ヘッダー無しのCSVデータ
```text
1,"高橋","たかはし"
2,"鈴木","すずき"
3,"佐藤","さとう"
4,"田中","たなか"
5,"佐々木","ささき"
```
これで先ほどのメソッドの以下のところをコメント化して実行してみました
```C#
    ////headerを読み込むかどうか
    //if (hasHeader)
    //{
    //    csv.Read();
    //    csv.ReadHeader();
    //}
```
結果は...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/234f0e2e-8fbf-80d9-0d14-22f783ee2627.png)
なぜか1行目が読み込まれず2行目から読み込まれて、4レコードとなりました。

csv.ReadHeader()を実行していないのに、1行目がヘッダーとして扱われてスキップ？されています。
不可解でいろいろと調べていくうちにある程度分かってきました。

# ヘッダーの読み込みの有無
ヘッダーを読み込むかどうかについて、csv.ReadHeader()を実行していましたが、どうやらデフォルトで実行されているようです。
理由は以下の値です
```C#
    csv.Configuration.HasHeaderRecord
```
これ昔のCsvHelperだと値(true/false)を設定していたのですが、現在のバージョンではデフォルトでtrueとなっています。
これを変更しないと、ReadHeader()に関係なくヘッダーがあるものとして扱われるようです。

### Configuration.HasHeaderRecordの変更方法
これは実は簡単で以下のような感じでConfigを作成して、CsvReaderをインスタンス化する際に読み込ませます。
```C#
    var config = new CsvConfiguration(CultureInfo.CurrentCulture)
    {
        HasHeaderRecord = hasHeader
    };
    using var csv = new CsvReader(reader, config);
```
これでヘッダーの読み込みの有無を決定できます。
さらに、さきほどの読み込み結果で分かるように、実はcsv.ReadHeader()を実行する必要はないようです。

ということで、最終的に完成したメソッドは以下のようになりました。
```C#
public static IEnumerable<T> ReadCsv<T>(string filePath, bool hasHeader, string encode = "utf-8")
{
    var encodingProvider = System.Text.CodePagesEncodingProvider.Instance;

    Encoding? encoding = encode switch
    {
        "utf-8" => Encoding.UTF8,
        "shift-jis" => encodingProvider.GetEncoding(932), // 932=Shift-JIS
        _ => Encoding.UTF8
    };

    Console.WriteLine("CsvHelperの読み込み");
    using var reader = new StreamReader(filePath, encoding!);

    var config = new CsvConfiguration(CultureInfo.CurrentCulture)
    {
        HasHeaderRecord = hasHeader
    };
    using var csv = new CsvReader(reader, config);

    return csv.GetRecords<T>().ToList();
}
```

CsvHelperの動作としては全行を必ず読み込みます。
そしてConfiguration.HasHeaderRecord=trueをデフォルトとして、ヘッダーが存在するものとしてデータを取り扱います。
その中で一行目をヘッダーとして取り扱う場合はReadHeaderを実行し、そうでない(ReadHeaderを実行しない)場合は、一行目をスキップするわけです。
従ってヘッダーが存在しないCSVデータを取り扱う場合は、ReadHeaderを実行しないのではなく、Configuration.HasHeaderRecordをfalseにしてデータの状態を設定する必要があるという事でした。

解決してしまえば何のことはないのですが、Web記事など見ていくと迷う事ばかりなので、同じように迷っている方の助けになれば幸いです。