Допустим, у вас есть конечная точка, которая возвращает ответ 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, я использую Mongo в качестве постоянного хранилища данных. Какое отношение одно имеет к другому?
Не уверен, как мы можем помочь, если вы немного не объясните структуру. Например, чем один ответ отличается от другого? Какие предположения мы можем сделать?
@PiotrJerzyMamenas, в вашем посте не упоминается Монго
Кроме того, это не очень хорошо отформатированный JSON.
@saj одно не имеет ничего общего с другим, так с чего бы это? И это тоже только часть json...
@PiotrJerzyMamenas, если вы имеете в виду какое-либо отношение к Монго, я уже сказал, что вы не упоминали об этом ранее, я не уверен, на что вы намекаете и как это помогает в вашем вопросе.
@saj Ну, я также не упомянул, что использую токены jwt для авторизации, но это потому, что этот пост не имеет ничего общего с токенами jwt или авторизацией ..... Точно так же он не имеет ничего общего с хранилищами данных ... Этот вопрос касается Разбор ответа http не об объектно-реляционных преобразователях или хранилищах данных, EF Core или Mongo могут ПОЛНОСТЬЮ не существовать здесь, и я мог бы просто сохранить это в текстовый файл на диске или вообще не сохранять его и просто вывести его на консоль...
Это своеобразный формат, особенно с массивами данных, содержащими различные типы. Однако вы можете использовать такой простой класс:
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);
Да, это подход, который я имел в виду с отражением. Спасибо, посмотрим, есть ли у других людей другие идеи.
Я почти уверен, что другого способа сделать это нет, нет встроенного десериализатора, поэтому вам нужно либо использовать определенный код, либо использовать отражение, чтобы иметь возможность обрабатывать любой тип.
Я решил пойти на решение для отражения, но немного скорректировал. Я использую 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 для чисел с плавающей запятой.
Вы смотрели EF Core?