Как десериализовать ответ API с учетом динамического списка столбцов? C # .NET Core 2.2 - PullRequest
0 голосов
/ 30 мая 2019

Скажем, у вас есть конечная точка, которая возвращает 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
  ]
]

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

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]
  ..
}

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

Ответы [ 2 ]

1 голос
/ 30 мая 2019

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

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);
0 голосов
/ 01 июня 2019

Я решил пойти на решение отражения, но немного отрегулировано. Я использую 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 не содержит определения методов индексации, вы не можете использовать оператор []. Было бы здорово, если бы вы могли настроить его. Кроме того, я бы лично предложил использовать десятичный тип для чисел с плавающей запятой.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...