Как десериализовать ответ API с учетом динамического списка столбцов? С# .NET Core 2.2

Допустим, у вас есть конечная точка, которая возвращает ответ JSON с динамическим списком индексированных столбцов в следующем формате:

"columnNames": [
  "date",
  "value",
  "someOtherValue"
],
"data": [
  [
    "2019-05-29",
    1.23,
    2.34
  ],
  [
    "2019-05-28",
    0.20,
    1.34
  ],
  [
    "2019-05-27,
    2.99,
    1.94
  ]
]

Каким будет наиболее оптимальный способ десериализации такого ответа? Я мог бы попробовать сопоставить его с каким-то классом, который будет содержать как имена столбцов, так и данные, а затем просто отобразить его более или менее так (псевдокод):

var apiResponseContent = await response.Content.ReadAsAsync<ApiResponse>();
foreach(var responseData in apiResponseContent.data) {
  var model = new Model();
  model.date = responseData[apiResponseContent.columnNames.First(v => v == "date").index]
  ..
}

Но это кажется настолько типичным сценарием, что должна быть более эффективная альтернатива, которая не будет ломаться, если ответ конечной точки изменится. Конечно, я мог бы использовать отражение и создать метод расширения для автоматического сопоставления столбцов с классами, но мне это кажется странным.

Вы смотрели EF Core?

fuzzybear 30.05.2019 16:44

Я не использую EF Core, я использую Mongo в качестве постоянного хранилища данных. Какое отношение одно имеет к другому?

qubits 30.05.2019 16:45

Не уверен, как мы можем помочь, если вы немного не объясните структуру. Например, чем один ответ отличается от другого? Какие предположения мы можем сделать?

DavidG 30.05.2019 16:45

@PiotrJerzyMamenas, в вашем посте не упоминается Монго

fuzzybear 30.05.2019 16:46

Кроме того, это не очень хорошо отформатированный JSON.

DavidG 30.05.2019 16:47

@saj одно не имеет ничего общего с другим, так с чего бы это? И это тоже только часть json...

qubits 30.05.2019 16:49

@PiotrJerzyMamenas, если вы имеете в виду какое-либо отношение к Монго, я уже сказал, что вы не упоминали об этом ранее, я не уверен, на что вы намекаете и как это помогает в вашем вопросе.

fuzzybear 30.05.2019 16:54

@saj Ну, я также не упомянул, что использую токены jwt для авторизации, но это потому, что этот пост не имеет ничего общего с токенами jwt или авторизацией ..... Точно так же он не имеет ничего общего с хранилищами данных ... Этот вопрос касается Разбор ответа http не об объектно-реляционных преобразователях или хранилищах данных, EF Core или Mongo могут ПОЛНОСТЬЮ не существовать здесь, и я мог бы просто сохранить это в текстовый файл на диске или вообще не сохранять его и просто вывести его на консоль...

qubits 30.05.2019 17:01
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
1 310
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это своеобразный формат, особенно с массивами данных, содержащими различные типы. Однако вы можете использовать такой простой класс:

public class ApiResponse
{
    public IEnumerable<string> ColumnNames { get; set; }
    public IEnumerable<List<object>> Data { get; set; }
}

Если вы хотите сопоставить этот объект с другим набором объектов, вы можете использовать общую функцию, которая использует отражение, например:

public List<T> MapTo<T>(ApiResponse source) 
    where T : new()
{
    var properties = typeof(T).GetProperties();

    foreach (var datum in source.Data)
    {
        var t = new T();

        for(var colIndex = 0; colIndex < source.ColumnNames.Count; colIndex++)
        {
            var property = properties.SingleOrDefault(p => p.Name.Equals(source.ColumnNames[colIndex], StringComparison.InvariantCultureIgnoreCase));
            if (property != null)
            {
                property.SetValue(t, Convert.ChangeType(datum[colIndex], property.PropertyType));
            }
        }
        yield return t;
    }
}

И ваш окончательный код может выглядеть примерно так:

public class Foo
{
    public string Date { get; set; }
    public double Value { get; set; }
    public double SomeOtherValue { get; set; }
}


var apiResponseContent = await response.Content.ReadAsAsync<ApiResponse>();
var actualData = MapTo<Foo>(apiResponseContent);

Да, это подход, который я имел в виду с отражением. Спасибо, посмотрим, есть ли у других людей другие идеи.

qubits 30.05.2019 17:09

Я почти уверен, что другого способа сделать это нет, нет встроенного десериализатора, поэтому вам нужно либо использовать определенный код, либо использовать отражение, чтобы иметь возможность обрабатывать любой тип.

DavidG 31.05.2019 15:04

Я решил пойти на решение для отражения, но немного скорректировал. Я использую AutoMapper в проекте, поэтому для поддержания архитектурной согласованности я написал ValueResolver, который по существу обертывает ваш подход, всего с несколькими настройками:

public class VariableColumnConverter : ITypeConverter<ApiResponse, List<AssetPrice>>
{
    public List<AssetPrice> Convert(ApiResponse source, List<AssetPrice> destination, ResolutionContext context)
    {
        var properties = typeof(AssetPrice).GetProperties();
        destination = new List<AssetPrice>();

        foreach (var dataItem in source.data)
        {
            var price = new AssetPrice();

            foreach (var column in source.columnNames.Select((value, i) => (value, i)))
            {
                var property = properties.SingleOrDefault(p => p.Name.Equals(column.value, StringComparison.InvariantCultureIgnoreCase));

                if (property != null)
                {
                    property.SetValue(price, System.Convert.ChangeType(dataItem[column.i], property.PropertyType));
                }
            }
            destination.Add(price);
        }

        return destination;
    }
}

Спасибо @DavidG, я удалил универсальный, так как у меня есть только одна такая конечная точка. Я отмечу ваш ответ как правильный, так как мой конечный подход немного самоуверен, и корень использует ваш подход.

Несмотря на это, в вашем коде есть небольшие проблемы, из-за которых он не компилируется, Root на самом деле будет ApiResponse, а также, поскольку IEnumerable не содержит определения методов индексации, вы не можете использовать оператор []. Было бы здорово, если бы вы могли его отрегулировать. Кроме того, я бы лично предложил использовать тип Decimal для чисел с плавающей запятой.

Другие вопросы по теме