Я готовлю миграцию с ASP.NET Core 2.2 на 3.0.
Поскольку я не использую более продвинутые функции JSON (но, возможно, одну, как описано ниже), а версия 3.0 теперь поставляется со встроенным пространством имен/классами для JSON, System.Text.Json, я решил посмотреть, смогу ли я отказаться от предыдущего значения по умолчанию Newtonsoft.Json.
Обратите внимание, я знаю, что System.Text.Json не заменит полностью Newtonsoft.Json.
Мне удалось сделать это везде, например.
var obj = JsonSerializer.Parse<T>(jsonstring);
var jsonstring = JsonSerializer.ToString(obj);
но в одном месте, где я заполняю существующий объект.
С Newtonsoft.Json можно сделать
JsonConvert.PopulateObject(jsonstring, obj);
Во встроенном пространстве имен System.Text.Json есть несколько дополнительных классов, таких как JsonDocumnet, JsonElement и Utf8JsonReader, хотя я не могу найти ни одного, который принимает существующий объект в качестве параметра.
Я также не достаточно опытен, чтобы увидеть, как использовать существующий.
Там может быть возможная предстоящая функция в .Net Core (спасибо Мустафа Гурсель за ссылку), но пока (а что, если его нет)...
...Теперь мне интересно, можно ли добиться чего-то подобного тому, что можно сделать с PopulateObject?
Я имею в виду, возможно ли с любым другим классом System.Text.Json выполнить то же самое и обновить/заменить только установленные свойства?,... или какой-то другой умный обходной путь?
Вот пример ввода/вывода того, что я ищу, и он должен быть универсальным, поскольку объект, переданный в метод десериализации, имеет тип <T>). У меня есть 2 строки Json для анализа в объект, где у первой установлены некоторые свойства по умолчанию, а у второй - некоторые, например.
Обратите внимание, что значение свойства может быть любого другого типа, кроме string.
JSON-строка 1:
{
"Title": "Startpage",
"Link": "/index",
}
JSON-строка 2:
{
"Head": "Latest news"
"Link": "/news"
}
Используя 2 строки Json выше, я хочу, чтобы объект приводил к:
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
Как видно из приведенного выше примера, если свойства во 2-м имеют значения/установлены, они заменяют значения в 1-м (как с «Заголовок» и «Ссылка»), если нет, существующее значение сохраняется (как с «Заголовок»)





Я мало что знаю об этой новой версии плагина, однако я нашел руководство, которому можно следовать учебник с некоторыми примерами
Основываясь на нем, я придумал этот метод и представляю, что он способен решить свою проблему.
//To populate an existing variable we will do so, we will create a variable with the pre existing data
object PrevData = YourVariableData;
//After this we will map the json received
var NewObj = JsonSerializer.Parse<T>(jsonstring);
CopyValues(NewObj, PrevData)
//I found a function that does what you need, you can use it
//source: https://stackoverflow.com/questions/8702603/merging-two-objects-in-c-sharp
public void CopyValues<T>(T target, T source)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source== null) throw new ArgumentNullException(nameof(source));
Type t = typeof(T);
var properties = t.GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(prop =>
prop.CanRead
&& prop.CanWrite
&& prop.GetIndexParameters().Length == 0);
foreach (var prop in properties)
{
var value = prop.GetValue(source, null);
prop.SetValue(target, value, null);
}
}
Будете ли вы всегда получать объект любого вида, всегда ли он будет неопределенным?
Да, любой объект, нет, иногда неопределенный, иногда нет, что показывает мой образец строки json.
Давайте продолжить обсуждение в чате.
Вы ищете способ скопировать свойства какого-либо объекта в другой, к JSON это не имеет никакого отношения.
но цель не в том, чтобы унифицировать json, а в несериализованном объекте
Не поймите меня неправильно. Ваш ответ наиболее близок к решению. Я имел в виду ОП.
Ладно, постараюсь максимально улучшить.
Я вижу пару недостатков. Можем ли мы войти в чат, чтобы обсудить их?
Спасибо за ваше редактирование. Посмотрим на это позже, в конце баунти-периода.
Если вы уже используете Автокартограф в своем проекте или не возражаете против зависимости от него, вы можете объединить объекты следующим образом:
var configuration = new MapperConfiguration(cfg => cfg
.CreateMap<Model, Model>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != default)));
var mapper = configuration.CreateMapper();
var destination = new Model {Title = "Startpage", Link = "/index"};
var source = new Model {Head = "Latest news", Link = "/news"};
mapper.Map(source, destination);
class Model
{
public string Head { get; set; }
public string Title { get; set; }
public string Link { get; set; }
}
Хотите объяснить голосование против? Очевидно, что эта функциональность еще не реализована в .NET Core 3.0. Таким образом, в основном есть два подхода: либо создать какую-то пользовательскую реализацию, либо использовать существующий инструмент, который может выполнить эту работу.
он всегда будет получать объект Т-типа, он не может создать для него фиксированный класс
@SuperPenguino Я предполагаю, что в проекте необходимо объединить определенное количество объектов. Таким образом, должна быть возможность зарегистрировать их при запуске приложения. Даже автоматически по соглашению.
Я не использую AutoMapper, и, если нужно, я лучше придерживаюсь Newtonsoft, который больше подходит для работы. Я хочу/предпочитаю использовать встроенные инструменты для работы с данными Json. И я не понизил голос, так как это может действительно сработать, хотя это не то, что я ищу.
@LGSon Я проверил это перед публикацией, для этого потребуется зарегистрировать все вложенные типы в графе, но это можно сделать автоматически и определенно будет работать. Конечно, если есть возможность, лучше использовать Newtonsoft, особенно если вы не используете AutoMapper.
Я не уверен, решит ли это вашу проблему, но это должно работать как временное решение. Все, что я сделал, это написал простой класс с методом populateobject.
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
Я поместил его в консольное приложение для тестирования. Ниже приведено все приложение, если вы хотите протестировать его самостоятельно.
using System;
using System.Text.Json;
using System.IO;
using System.Text.Json.Serialization;
namespace JsonQuestion1
{
class Program
{
static void Main(string[] args)
{
// Only used for testing
string path = @"C:\Users\Path\To\JsonFiles";
string st1 = File.ReadAllText(path + @"\st1.json");
string st2 = File.ReadAllText(path + @"\st2.json");
// Only used for testing ^^^
string myObject = MyDeserializer.PopulateObject(new[] { st1, st2 } );
Console.WriteLine(myObject);
Console.ReadLine();
}
}
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
}
Содержимое JSON-файла:
st1.json
{
"Title": "Startpage",
"Link": "/index"
}
st2.json
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
Обратите внимание, что многоуровневый json, вероятно, сломает этот код.
Извините, это не значит, что вы сказали, что я проголосовал против, просто хотел упомянуть об этом (и я удалил этот комментарий сейчас). Спасибо за ваш ответ, с его простотой у него есть свои преимущества, и я посмотрю на него позже.
И кстати, после просмотра других ответов, в качестве исправления многоуровневого json можно было бы проверить объекты тип значения, а если нет IsValueType, то выполнить рекурсивный вызов.
Я понимаю, что могу заставить его поддерживать многоуровневый JSON, но это выходит за рамки вопроса. Я пытался дать самое простое решение проблемы, чтобы не было слишком много беспорядка.
Чем проще/чище, тем лучше, и легкое исправление я могу сделать сам, если это необходимо.
Итак, если предположить, что Core 3 не поддерживает это из коробки, давайте попробуем обойти эту проблему. Итак, в чем наша проблема?
Нам нужен метод, который перезаписывает некоторые свойства существующего объекта свойствами из строки json. Таким образом, наш метод будет иметь сигнатуру:
void PopulateObject<T>(T target, string jsonSource) where T : class
Нам не нужен какой-либо пользовательский синтаксический анализ, так как он громоздкий, поэтому мы попробуем очевидный подход — десериализовать jsonSource и скопировать свойства результата в наш объект. Однако мы не можем просто пойти
T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);
Это потому, что для типа
class Example
{
int Id { get; set; }
int Value { get; set; }
}
и JSON
{
"Id": 42
}
мы получим updateObject.Value == 0. Теперь мы не знаем, является ли 0 новым обновленным значением или оно просто не было обновлено, поэтому нам нужно точно знать, какие свойства содержит jsonSource.
К счастью, System.Text.Json API позволяет нам изучить структуру проанализированного JSON.
using var json = JsonDocument.Parse(jsonSource).RootElement;
Теперь мы можем перечислить все свойства и скопировать их.
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
Мы скопируем значение, используя отражение:
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
Здесь мы видим, что мы делаем обновление мелкий. Если объект содержит в качестве свойства другой сложный объект, он будет скопирован и перезаписан целиком, а не обновлен. Если вам требуются обновления глубокий, этот метод необходимо изменить, чтобы извлечь текущее значение свойства, а затем рекурсивно вызвать PopulateObject, если тип свойства является ссылочным типом (для этого также потребуется принять Type в качестве параметра в PopulateObject).
Соединив все вместе, мы получим:
void PopulateObject<T>(T target, string jsonSource) where T : class
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
}
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
Насколько это надежно? Ну, это, конечно, не сделает ничего разумного для массива JSON, но я не уверен, как вы ожидаете, что метод PopulateObject будет работать с массивом для начала. Я не знаю, насколько он сравним по производительности с версией Json.Net, вам придется проверить это самостоятельно. Он также автоматически игнорирует свойства, не относящиеся к целевому типу, по замыслу. Я думал, что это самый разумный подход, но вы можете подумать иначе, в этом случае свойство null-check должно быть заменено генерацией исключения.
Обновлено:
Я пошел дальше и реализовал глубокую копию:
void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
void PopulateObject(object target, string jsonSource, Type type)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property, type);
}
}
void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
Чтобы сделать это более надежным, вам нужно либо иметь отдельный метод PopulateObjectDeep, либо передать PopulateObjectOptions или что-то подобное с флагом deep/shallow.
Обновлено еще раз:
Смысл глубокого копирования в том, что если у нас есть объект
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 32
},
"Value": 128
}
и заполните его
{
"Child":
{
"Value": 64
}
}
мы бы получили
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 64
},
"Value": 128
}
В случае мелкой копии мы получим Id = 0 в скопированном дочернем элементе.
РЕДАКТИРОВАТЬ 3:
Как указал @ldam, это больше не работает в стабильной версии .NET Core 3.0, поскольку API был изменен. Метод Parse теперь Deserialize, и вам нужно копнуть глубже, чтобы добраться до значения JsonElement. Существует активная проблема в репозитории corefx для прямой десериализации JsonElement. Прямо сейчас самое близкое решение — использовать GetRawText(). Я пошел дальше и отредактировал приведенный выше код, чтобы он работал, оставив старую версию зачеркнутой.
Разве глубокое копирование не является излишним для этого варианта использования? Поскольку копируемый объект уже является десериализованным клоном объекта.
@dfhwze Боюсь, я не понимаю. Вы хотите уточнить?
Вместо рекурсивного глубокого копирования всех свойств-потомков достаточно скопировать только дочерние элементы из а в б. Нет причин для глубокого копирования, потому что а — это десериализованный экземпляр, свойства потомков которого не требуют сохранения после копирования в б. Я надеюсь это имеет смысл.
Но смысл глубокой копии — обновить потомков, а не заменить их. Я добавил редактирование, которое должно прояснить это.
Ага! Я понимаю, что вы имеете ввиду. Весь граф исходных объектов может быть частичным подмножеством графа целевых объектов. Это имеет смысл.
Я награжу вас первоначальной наградой, так как этот ответ дал мне хорошее представление о том, что можно сделать с помощью JsonDocument, что также является основной частью моего вопроса. Спасибо за ваш ответ.
Это не совсем работает сейчас, когда .NET Core 3 вышел из предварительной версии.
@ldam Да, исправил.
Привет еще раз. Я столкнулся с проблемой, когда значение объекта может быть массивом (со значениями или объектами). При этом строка foreach (var property in json.EnumerateObject()) с исключением. Как мне перечислить значения массива и получить его объект/значения? ... И то, что я спрашиваю здесь, входит в рамки этого вопроса, или я должен запостить новый?
@Asons Я бы задал отдельный вопрос, чтобы получить больше информации.
Я сделал здесь новый вопрос. У меня до сих пор нет правильного ответа, поэтому я надеюсь, что он у вас есть.
Если это только одно использование, и вы не хотите добавлять дополнительные зависимости/много кода, вы не возражаете против небольшой неэффективности а также Я не пропустил что-то очевидное, вы можете просто использовать:
private static T ParseWithTemplate<T>(T template, string input)
{
var ignoreNulls = new JsonSerializerOptions() { IgnoreNullValues = true };
var templateJson = JsonSerializer.ToString(template, ignoreNulls);
var combinedData = templateJson.TrimEnd('}') + "," + input.TrimStart().TrimStart('{');
return JsonSerializer.Parse<T>(combinedData);
}
Спасибо, позже проверим, разрешает ли JsonSerializer одно и то же свойство дважды в строке json. Сначала мне это нравилось, пока я не нашла метод Newtonsoft. Что может быть неясно в моем вопросе, так это то, что я выполняю десериализацию шаблон и Вход одновременно, поэтому этот трюк менее неэффективен, чем кажется, поскольку мне не нужны дополнительные JsonSerializer.ToString();. Я также кеширую результат и не запускаю его, если какая-либо из строк не отредактирована, что делает его еще менее проблематичным с небольшая неэффективность.
Кажется, это разрешено в 3.0 preview6, я думаю, неэффективно проверять наличие дубликатов при анализе. Недостатком является слияние только «верхнего уровня» объектов, поэтому это не удастся, если вам нужно объединить свойство сложного типа/массива между шаблон и Вход.
Я знаю об этом недостатке ... у меня была такая же проблема с моей собственной версией, тем не менее, я могу с этим смириться, если это сводится к тому, чтобы быть лучшим вариантом.
Вот пример кода, который это делает. Он использует новый Структура Utf8JsonReader, поэтому он заполняет объект одновременно с его анализом. Он поддерживает эквивалентность типов JSON/CLR, вложенные объекты (создает, если они не существуют), списки и массивы.
var populator = new JsonPopulator();
var obj = new MyClass();
populator.PopulateObject(obj, "{\"Title\":\"Startpage\",\"Link\":\"/index\"}");
populator.PopulateObject(obj, "{\"Head\":\"Latest news\",\"Link\":\"/news\"}");
public class MyClass
{
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
}
Обратите внимание, что он не поддерживает все, что вы, вероятно, ожидаете, но вы можете переопределить или настроить его. Что можно добавить: 1) соглашение об именах. Вам придется переопределить метод GetProperty. 2) словари или объекты расширения. 3) производительность может быть улучшена, поскольку она использует Reflection вместо методов MemberAccessor/delegate.
public class JsonPopulator
{
public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
{
options ??= new JsonSerializerOptions();
var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
new Worker(this, reader, obj, options);
}
protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = obj.GetType().GetProperty(propertyName);
return prop;
}
protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = GetProperty(ref reader, options, obj, propertyName);
if (prop == null)
return false;
if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
return false;
prop.SetValue(obj, value);
return true;
}
protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (reader.TokenType == JsonTokenType.Null)
{
value = null;
return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
}
if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
if (propertyType == typeof(string)) { value = JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); return true; }
if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }
if (propertyType == typeof(bool))
{
if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
{
value = reader.GetBoolean();
return true;
}
}
// fallback here
return TryConvertValue(ref reader, propertyType, out value);
}
protected virtual object ReadValue(ref Utf8JsonReader reader)
{
switch (reader.TokenType)
{
case JsonTokenType.False: return false;
case JsonTokenType.True: return true;
case JsonTokenType.Null: return null;
case JsonTokenType.String: return reader.GetString();
case JsonTokenType.Number: // is there a better way?
if (reader.TryGetInt32(out var i32))
return i32;
if (reader.TryGetInt64(out var i64))
return i64;
if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
return ui64;
if (reader.TryGetSingle(out var sgl))
return sgl;
if (reader.TryGetDouble(out var dbl))
return dbl;
if (reader.TryGetDecimal(out var dec))
return dec;
break;
}
throw new NotSupportedException();
}
// we're here when json types & property types don't match exactly
protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (propertyType == typeof(bool))
{
if (reader.TryGetInt64(out var i64)) // one size fits all
{
value = i64 != 0;
return true;
}
}
// TODO: add other conversions
value = null;
return false;
}
protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
{
if (propertyType.GetConstructor(Type.EmptyTypes) == null)
return null;
// TODO: handle custom instance creation
try
{
return Activator.CreateInstance(propertyType);
}
catch
{
// swallow
return null;
}
}
private class Worker
{
private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
private readonly Stack<object> _objects = new Stack<object>();
public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
{
_objects.Push(obj);
WorkerProperty prop;
WorkerProperty peek;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
prop = new WorkerProperty();
prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
_properties.Push(prop);
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
if (_properties.Count > 0)
{
object child = null;
var parent = _objects.Peek();
PropertyInfo pi = null;
if (parent != null)
{
pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
if (pi != null)
{
child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
if (child == null && pi.CanWrite)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
break; // don't create if we can't handle it
}
if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
{
child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
}
else
{
child = populator.CreateInstance(ref reader, pi.PropertyType);
if (child != null)
{
pi.SetValue(parent, child);
}
}
}
}
}
if (reader.TokenType == JsonTokenType.StartObject)
{
_objects.Push(child);
}
else if (child != null) // StartArray
{
peek = _properties.Peek();
peek.IsArray = pi.PropertyType.IsArray;
peek.List = (IList)child;
peek.ListPropertyType = GetListElementType(child.GetType());
peek.ArrayPropertyInfo = pi;
}
}
break;
case JsonTokenType.EndObject:
_objects.Pop();
if (_properties.Count > 0)
{
_properties.Pop();
}
break;
case JsonTokenType.EndArray:
if (_properties.Count > 0)
{
prop = _properties.Pop();
if (prop.IsArray)
{
var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
prop.List.CopyTo(array, 0);
prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
}
}
break;
case JsonTokenType.False:
case JsonTokenType.Null:
case JsonTokenType.Number:
case JsonTokenType.String:
case JsonTokenType.True:
peek = _properties.Peek();
if (peek.List != null)
{
if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
{
peek.List.Add(item);
}
break;
}
prop = _properties.Pop();
var current = _objects.Peek();
if (current != null)
{
populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
}
break;
}
}
}
private static Type GetListElementType(Type type)
{
if (type.IsArray)
return type.GetElementType();
foreach (Type iface in type.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
}
return typeof(object);
}
}
private class WorkerProperty
{
public string PropertyName;
public IList List;
public Type ListPropertyType;
public bool IsArray;
public PropertyInfo ArrayPropertyInfo;
public override string ToString() => PropertyName;
}
}
Я начал 2-ю награду и присужу ваш ответ (нужно подождать 24 часа, пока я не смогу), так как это дало мне хорошее представление о том, что можно сделать с помощью Utf8JsonReader, что также является основной частью моего вопроса. Спасибо за ваш ответ.
Пробовал использовать JsonPopulator, но получил ошибку: «JsonSerializer» не содержит определения для «ReadValue».
@tb-mtg - да, похоже, ReadValue был переименован в Serialize между бета-версией .NET core 3 и финальным выпуском: github.com/dotnet/corefx/commit/… Я обновил свой ответ.
Я пытался использовать это, но когда я пытаюсь утверждать, что ваш MyClass может быть передан туда и обратно без потери данных, мое утверждение терпит неудачу, потому что значения строковых свойств добавляются в двойные кавычки. См. dotnetfiddle.net/BsAeIu. Исправление похоже на вызов JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); вместо GetRawText(), см. dotnetfiddle.net/Q7SxQp
@dbc - спасибо, что указали на это. Это странно, я на 100% уверен, что тестировал это в начальной версии, так что с тех пор что-то изменилось в классах json. Я обновил код.
Обходной путь также может быть таким же простым (также поддерживает многоуровневый JSON):
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
namespace ConsoleApp
{
public class Model
{
public Model()
{
SubModel = new SubModel();
}
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
public SubModel SubModel { get; set; }
}
public class SubModel
{
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
var model = new Model();
Console.WriteLine(JsonSerializer.ToString(model));
var json1 = "{ \"Title\": \"Startpage\", \"Link\": \"/index\" }";
model = Map<Model>(model, json1);
Console.WriteLine(JsonSerializer.ToString(model));
var json2 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Name\": \"Reyan Chougle\" } }";
model = Map<Model>(model, json2);
Console.WriteLine(JsonSerializer.ToString(model));
var json3 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Engineer\" } }";
model = Map<Model>(model, json3);
Console.WriteLine(JsonSerializer.ToString(model));
var json4 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Programmer\" } }";
model = Map<Model>(model, json4);
Console.WriteLine(JsonSerializer.ToString(model));
Console.ReadKey();
}
public static T Map<T>(T obj, string jsonString) where T : class
{
var newObj = JsonSerializer.Parse<T>(jsonString);
foreach (var property in newObj.GetType().GetProperties())
{
if (obj.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(newObj) != null))
{
if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == typeof(T).Assembly.FullName)
{
MethodInfo mapMethod = typeof(Program).GetMethod("Map");
MethodInfo genericMethod = mapMethod.MakeGenericMethod(property.GetValue(newObj).GetType());
var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(newObj), JsonSerializer.ToString(property.GetValue(newObj)) });
foreach (var property2 in obj2.GetType().GetProperties())
{
if (property2.GetValue(obj2) != null)
{
property.GetValue(obj).GetType().GetProperty(property2.Name).SetValue(property.GetValue(obj), property2.GetValue(obj2));
}
}
}
else
{
property.SetValue(obj, property.GetValue(newObj));
}
}
}
return obj;
}
}
}
Выход:
Комментарии не для расширенного обсуждения; этот разговор был перешел в чат.