c# asp.net-core razor-pages asp.net-core-2.2 asp.net-core-3.0

c# - .Net Core 3.0 JsonSerializer llena el objeto existente



asp.net-core razor-pages (7)

Estoy preparando una migración de ASP.NET Core 2.2 a 3.0.

Como no uso más funciones JSON avanzadas (pero tal vez una como se describe a continuación), y 3.0 ahora viene con un espacio de nombres / clases incorporado para JSON, System.Text.Json , decidí ver si podía eliminar el Newtonsoft.Json predeterminado anterior. Tenga en cuenta que soy consciente de que System.Text.Json no reemplazará completamente a Newtonsoft.Json .

Me las arreglé para hacer eso en todas partes, por ejemplo

var obj = JsonSerializer.Parse<T>(jsonstring); var jsonstring = JsonSerializer.ToString(obj);

pero en un solo lugar, donde puebla un objeto existente.

Con Newtonsoft.Json se puede hacer.

JsonConvert.PopulateObject(jsonstring, obj);

El System.Text.Json nombres System.Text.Json incorporado tiene algunas clases adicionales, como JsonDocumnet , JsonElement y Utf8JsonReader , aunque no puedo encontrar ninguna que tome un objeto existente como parámetro.

Tampoco tengo la experiencia suficiente para ver cómo hacer uso de los existentes.

Puede haber una posible función próxima en .Net Core (gracias a Mustafa Gursel por el enlace), pero mientras tanto (y si no es así), ...

... Ahora me pregunto, ¿es posible lograr algo similar a lo que se puede hacer con PopulateObject ?

Quiero decir, ¿es posible con alguna de las otras clases de System.Text.Json lograr lo mismo, y actualizar / reemplazar solo las propiedades establecidas?, ... o alguna otra solución inteligente?

Aquí hay una muestra de cómo podría verse (y debe ser genérico ya que el objeto pasado al método de deserialización es de tipo <T> ). Tengo 2 cadenas Json para ser analizadas en un objeto, donde la primera tiene un conjunto de propiedades predeterminadas y la segunda, por ejemplo,

Tenga en cuenta que un valor de propiedad puede ser de cualquier otro tipo que no sea una string .

Json cadena 1:

{ "Title": "Startpage", "Link": "/index", }

Json cadena 2:

{ "Head": "Latest news" "Link": "/news" }

Usando las cadenas 2 Json de arriba, quiero un objeto que resulte en:

{ "Title": "Startpage", "Head": "Latest news", "Link": "/news" }

Como se vio en la muestra anterior, si las propiedades en la 2ª tienen valores / se establecen, reemplaza los valores en la 1ª (como con "Head" y "Link"), si no, el valor existente persiste (como con "Title")


Aquí hay un código de ejemplo que lo hace. Está utilizando la nueva estructura Utf8JsonReader para que rellene el objeto al mismo tiempo que lo analiza. Admite equivalencia de tipos JSON / CLR, objetos anidados (se crea si no existen), listas y matrices.

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; } }

Tenga en cuenta que no admite todo lo que probablemente esperaría, pero puede anularlo o personalizarlo. Cosas que podrían agregarse: 1) convención de nombres. Tendrías que anular el método GetProperty. 2) Diccionarios o expando objetos. 3) el rendimiento se puede mejorar porque utiliza técnicas de Reflexión en lugar de Acceso / Delegación de Miembros

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.ReadValue<JsonElement>(ref reader, options).GetRawText(); 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; } }


Entonces, suponiendo que el Core 3 no admita esto fuera de la caja, tratemos de solucionar este problema. Entonces, ¿cuál es nuestro problema?

Queremos un método que sobrescriba algunas propiedades de un objeto existente con las de una cadena json. Así nuestro método tendrá una firma de:

void PopulateObject<T>(T target, string jsonSource) where T : class

Realmente no queremos un análisis personalizado, ya que es engorroso, así que intentaremos el enfoque obvio: deserializar jsonSource y copiar las propiedades del resultado en nuestro objeto. Sin embargo, no podemos ir

T updateObject = JsonSerializer.Parse<T>(jsonSource); CopyUpdatedProperties(target, updateObject);

Eso es porque para un tipo

class Example { int Id { get; set; } int Value { get; set; } }

y un JSON

{ "Id": 42 }

obtendremos updateObject.Value == 0 . Ahora no sabemos si 0 es el nuevo valor actualizado o si simplemente no se actualizó, por lo que necesitamos saber exactamente qué propiedades contiene jsonSource .

Afortunadamente, la API System.Text.Json nos permite examinar la estructura del JSON analizado.

var json = JsonDocument.Parse(jsonSource).RootElement;

Ahora podemos enumerar todas las propiedades y copiarlas.

foreach (var property in json.EnumerateObject()) { OverwriteProperty(target, property); }

Copiaremos el valor utilizando la reflexión:

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; var parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType); propertyInfo.SetValue(target, parsedValue); }

Podemos ver aquí que lo que estamos haciendo es una actualización superficial . Si el objeto contiene otro objeto complejo como su propiedad, ese será copiado y sobrescrito en su totalidad, no actualizado. Si necesita actualizaciones profundas , este método debe cambiarse para extraer el valor actual de la propiedad y luego llamar al PopulateObject recursiva si el tipo de la propiedad es un tipo de referencia (que también requerirá aceptar el Type como un parámetro en PopulateObject ).

Uniéndonos a todos juntos obtenemos:

void PopulateObject<T>(T target, string jsonSource) where T : class { 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; var parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType); propertyInfo.SetValue(target, parsedValue); }

¿Qué tan robusto es esto? Bueno, ciertamente no hará nada sensato para una matriz JSON, pero no estoy seguro de cómo esperaría que un método PopulateObject funcionara en una matriz para empezar. No sé cómo se compara en rendimiento con la versión Json.Net , tendría que probarlo usted mismo. También ignora silenciosamente las propiedades que no están en el tipo de destino, por diseño. Pensé que era el enfoque más sensato, pero podría pensar de otra manera, en ese caso, la propiedad null-check tiene que ser reemplazada con un lanzamiento de excepción.

EDITAR:

Seguí adelante e implementé una copia profunda:

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) { 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) { parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType); } else { parsedValue = propertyInfo.GetValue(target); PopulateObject(parsedValue, updatedProperty.Value, propertyType); } propertyInfo.SetValue(target, parsedValue); }

Para hacer que esto sea más sólido, tendría que tener un método separado de PopulateObjectDeep o pasar PopulateObjectOptions o algo similar con una bandera profunda / superficial.

EDIT 2:

El punto de copia profunda es que si tenemos un objeto

{ "Id": 42, "Child": { "Id": 43, "Value": 32 }, "Value": 128 }

y poblarlo con

{ "Child": { "Value": 64 } }

obtendríamos

{ "Id": 42, "Child": { "Id": 43, "Value": 64 }, "Value": 128 }

En el caso de una copia superficial, obtendríamos Id = 0 en el niño copiado.


La solución también puede ser tan simple como esto:

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); }

Salida:


No estoy seguro de si esto solucionará su problema, pero debería funcionar como una solución temporal. Todo lo que hice fue escribir una clase simple con un método populateobject en ella.

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); } }

Lo puse en una aplicación de consola para propósitos de prueba. A continuación se muestra la aplicación completa si desea probarlo usted mismo.

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); } } }

Contenido del archivo Json:

st1.json

{ "Title": "Startpage", "Link": "/index" }

st2.json

{ "Title": "Startpage", "Head": "Latest news", "Link": "/news" }


No sé mucho acerca de esta nueva versión del complemento, sin embargo, encontré un tutorial que puede seguirse con algunos ejemplos.

Basándome en él, pensé en este método y me imagino que es capaz de resolver su problema.

//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://.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); } }


Si es solo un uso y no desea agregar dependencias adicionales / mucho código, no le importa un poco de ineficiencia y no me he perdido algo obvio, solo puede usar:

using System; using System.Linq; using System.Text.Json.Serialization; namespace ConsoleApp { public class Model { public string Title { get; set; } public string Head { get; set; } public string Link { get; set; } } class Program { static void Main(string[] args) { var model = new Model(); Console.WriteLine(JsonSerializer.ToString(model)); var json1 = "{ /"Title/": /"Startpage/", /"Link/": /"/index/" }"; Map<Model>(model, json1); Console.WriteLine(JsonSerializer.ToString(model)); var json2 = "{ /"Head/": /"Latest news/", /"Link/": /"/news/" }"; Map<Model>(model, json2); Console.WriteLine(JsonSerializer.ToString(model)); Console.ReadKey(); } private 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)) { property.SetValue(obj, property.GetValue(newObj)); } } return obj; } } }


Si ya usa AutoMapper en su proyecto o no le importa tener dependencia en él, puede fusionar objetos de la siguiente manera:

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; } }