У меня есть проект с использованием CsvHelper, который я написал на VS2017 еще в 2017 году и тогда он успешно работал. На этой неделе я пытаюсь перетащить код в VS2022, .Net Core и последнюю версию CsvHelper (33.0.1). При этом я обнаружил странную ошибку в CsvHelper.
В основном я использую функцию Получить записи классов, и у меня есть класс, который охватывает записи в моих данных CSV и соответствует регистру каждого заголовка и т. д. (и это работало в 2017 году). Например:
public class Foo
{
public int MyInt { get; set; }
public string MyString { get; set; }
public ComputedClass CustomData {get; set; }
...
}
Где «CustomData» — это класс, который я вычисляю на основе некоторых полей в «Foo», поэтому он не включается в необработанные данные CSV.
У меня также есть класс сопоставления CsvHelper для Foo, где я говорю CsvHelper игнорировать поле «CustomData» (это новое в моем коде VS2022, и я сделал это, чтобы попытаться устранить проблему, с которой я столкнулся)
public sealed class FooMap : ClassMap<Foo>
{
public FooMap()
{
AutoMap(CultureInfo.InvariantCulture);
Map(m => m.CustomData).Ignore();
}
}
Соответствующие данные CSV (но в необработанном файле CSV нет имени поля «CustomData», и это те же данные, которые я тестировал в 2017 году)
MyInt,MyString,...
1,"One",...
2,"Two",...
И я передаю эту конфигурацию в CsvHelper и вызываю программу чтения следующим образом (где «читатель» — это программа чтения текстовых файлов, которая открыла файл CSV.
CsvConfiguration config = new(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
TrimOptions = TrimOptions.Trim,
MissingFieldFound = null
};
using var csv = new CsvReader(reader, config);
csv.Context.RegisterClassMap<FooMap>();
while (csv.Read())
{
var row = csv.GetRecord<Foo>();
row.CustomData = new ComputedClass(row.MyInt,row.MyString);
...
}
И вот здесь начинается веселье. При выполнении этого кода GetRecord() генерирует исключение, связанное с заголовками, и сообщение об исключении начинается с:
Header with name 'myIntParam'[0] was not found.
Header with name 'myStringParam'[0] was not found.
...
Поля, о которых сообщается, что они отсутствуют, не имеют того же регистра, что поля, которые я определил в классе Foo, и не соответствуют тому, что находится в реальном файле CSV. Я выполнил полный поиск своего решения с учетом регистра, и у меня есть символы «myIntParam» и «myStringParam», но они используются только в качестве параметров конструктора «ComputedClass».
public ComputedClass(int myIntParam, string myStringParam)
{
...
}
Таким образом, кажется, что даже несмотря на то, что я говорю CsvHelper игнорировать свойство CustomData, CsvHelper все равно находит параметры конструктора для этого свойства. Но поскольку они являются параметрами конструктора, их нельзя добавить в класс FooMap и игнорировать.
Как устранить это исключение?
Согласно этому ответу CsvHelper 27.1.1 будет использовать конструктор без параметров, если он присутствует. Если нет, он будет использовать конструктор с наибольшим количеством параметров. Есть ли у вас конструктор без параметров, определенный для этого класса? Я думаю, вам это не понадобится, если вы проигнорируете это, так что это немного раздражает.
@mason Я не определил никакого явного конструктора для Foo, поэтому он использует конструктор без параметров по умолчанию, предоставляемый компилятором. Но у ComputedClass есть явный конструктор с параметрами. Теперь я думаю, что, возможно, обнаружил настоящую ошибку в CsvHelper из-за того, что отражение зашло слишком далеко при построении списка заголовков, ожидаемых в файле CSV.
@mason Еще мне, возможно, придется попробовать удалить конструктор из ComputedClass и сделать его без параметров. Тогда отражению будет нечего искать.
@mason Мой последний комментарий об удалении конструктора ComputedClass помог. Позже я напишу свой ответ. И сообщить об ошибке.
Основываясь на том, что @Mason предложил этот ответ на другой вопрос CsvHelper, я начал думать о том, как отражение, используемое CsvHelper, анализирует Foo, чтобы создать список заголовков, которые должны были быть в данных CSV.
По какой-то причине кажется, что CsvHelper немного переусердствовал и предполагал, что параметры конструктора ComputedClass должны быть заголовками. Моей первой попыткой исправить это было удаление параметров из конструктора и добавление функции Set(), которая принимала те же параметры. Это сработало.
Но потом я понял, что в моем коде есть еще один класс, который не вызывает проблем, хотя у него тоже есть параметризованный конструктор. Глядя на этот класс, я увидел, что определил явный конструктор по умолчанию наряду с параметризованным.
Я применил эту идею к ComputedClass, в результате чего получился код, который не вызывает проблемы:
// This satisfies CsvHelper when constructing the list of headers
[Obsolete()]
public ComputedClass()
{
}
// This is what I use in code, and CsvHelper now ignores
public ComputedClass(int myIntParam, string myStringParam)
{
...
}
Кроме того, я удалил ссылки FooMap и RegisterClassMap, и код продолжил работать.
Я просто искал способы пометить конструктор как «Не использовать», когда столкнулся с другим вопросом SO, в котором задавалась аналогичная вещь о (де)сериализаторах XML.
В строке не хватает закрывающей скобки
Map(m => m.CustomData.Ignore();