c# asp.net-web-api json.net

c# - Método genérico de modificación de JSON antes de ser devuelto al cliente



asp.net-web-api json.net (2)

De manera típica, el proceso de plantear la pregunta me llevó a tomar una nueva perspectiva del problema.

He encontrado una posible solución alternativa: crear un MediaTypeFormatter personalizado.

Con la ayuda de aquí y aquí , una posible solución:

using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http.Formatting; using System.Text; using System.Threading.Tasks; namespace Test { public class TestFormatter : MediaTypeFormatter { public TestFormatter() { SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); } public override bool CanReadType(Type type) { return false; } public override bool CanWriteType(Type type) { return true; } public override Task WriteToStreamAsync(Type type, object value, System.IO.Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext) { JsonSerializer serializer = new JsonSerializer(); serializer.ContractResolver = new CamelCasePropertyNamesContractResolver(); serializer.Converters.Add(new TestConverter()); return Task.Factory.StartNew(() => { using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, Encoding.ASCII)) { CloseOutput = false }) { serializer.Serialize(jsonTextWriter, value); jsonTextWriter.Flush(); } }); } } }

y luego configure la aplicación para usarla:

// insert at 0 so it runs before System.Net.Http.Formatting.JsonMediaTypeFormatter config.Formatters.Insert(0, new TestFormatter());

Esto crea una nueva instancia de mi JsonConverter para cada solicitud, que en combinación con las otras correcciones en la publicación original, parece resolver el problema.

Probablemente esta no sea la mejor manera de hacerlo, así que lo dejaré abierto para obtener mejores sugerencias o hasta que me dé cuenta de por qué esto no va a funcionar.

Busco un método genérico que me permita modificar el JSON de un objeto que se devuelve al cliente, específicamente la eliminación de ciertas propiedades en objetos devueltos. Similar a lo que se sugiere aquí .

Las modificaciones no son deterministas ya que se determinan por solicitud, en función de las reglas asociadas con el usuario. Por lo tanto, esto no es adecuado para un método que se almacena en caché.

Revisé varios métodos. La elección más obvia sería un JsonConverter, sin embargo, hay problemas con esto, como se detalla aquí , aquí y aquí .

El principal problema con este enfoque es que al llamar a JToken.FromObject en WriteJson para obtener el JSON para el valor específico, se llama recursivamente al mismo JsonConverter, lo que da como resultado un bucle.

He intentado con una variante de la solución que se incluye aquí, que proporciona un método para deshabilitar temporalmente CanWrite para evitar el problema de bucle. Sin embargo, parece que no funciona para más de una solicitud simultánea. Una instancia única de JsonConverter se está compartiendo entre varios subprocesos que están cambiando y leyendo el estado de la propiedad CanWrite en diferentes momentos, lo que provoca resultados incoherentes.

También intenté usar un serializador diferente en WriteJson (es decir, distinto del proporcionado al método); sin embargo, esto no es compatible con la recursión (porque ese serializador no usa mi JsonConverter), por lo que mis elementos anidados no son procesados ​​por mi JsonConverter. La eliminación de mi JsonConverter de la colección de conversores del serializador predeterminado tiene el mismo problema.

Básicamente, si quiero poder procesar de forma recursiva mi objeto modelo, obtendré el problema del ciclo de auto referencia.

Idealmente, JToken.FromObject tendría una forma selectiva de NO llamar al JsonConverter en el objeto mismo, pero aún aplicarlo a cualquier objeto hijo durante la serialización. A mitad de camino solucioné esto modificando CanConvert para establecer CanWrite en verdadero, solo si el objeto pasado a CanConvert era de un tipo diferente al último objeto pasado a WriteJson .

Sin embargo, para que esto funcione necesitaría un JsonConverter con alcance por solicitud (por las mismas razones de enhebrado anteriores), pero no veo cómo obtenerlo.

Aquí hay una muestra de lo que tengo:

using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Test { public class TestConverter : JsonConverter { bool CannotWrite { get; set; } public override bool CanWrite { get { return !CannotWrite; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JToken token; //---------------------------------------- // this works; but because it''s (i think) creating a new // serializer inside the FromObject method // which means any nested objects won''t get processed //token = JToken.FromObject(value); //---------------------------------------- // this creates loop because calling FromObject will cause this // same JsonConverter to get called on the same object again //token = JToken.FromObject(value, serializer); //---------------------------------------- // this gets around the loop issue, but the JsonConverter will // not apply to any nested objects //serializer.Converters.Remove(this); //token = JToken.FromObject(value, serializer); //---------------------------------------- // see https://stackoverflow.com/a/29720068/1196867 // // this works as it allows us to use the same serializer, but // temporarily sets CanWrite to false so the invocation of // FromObject doesn''t cause a loop // // this also means we can''t process nested objects, however // see below in CanConvert for a potential workaround. using (new PushValue<bool>(true, () => CannotWrite, (cantWrite) => CannotWrite = cantWrite)) { token = JToken.FromObject(value, serializer); } // store the type of this value so we can check it in CanConvert when called for any nested objects this.currentType = value.GetType(); //---------------------------------------- // in practice this would be obtained dynamically string[] omit = new string[] { "Name" }; JObject jObject = token as JObject; foreach (JProperty property in jObject.Properties().Where(p => omit.Contains(p.Name, StringComparer.OrdinalIgnoreCase)).ToList()) { property.Remove(); } token.WriteTo(writer); } private Type currentType; public override bool CanConvert(Type objectType) { if (typeof(Inua.WebApi.Authentication.IUser).IsAssignableFrom(objectType)) { // if objectType is different to the type which is currently being processed, // then set CanWrite to true, so this JsonConverter will apply to any nested // objects that we want to process if (this.currentType != null && this.currentType != objectType) { this.CannotWrite = false; } return true; } return false; } public override bool CanRead { get { return false; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } } }

Opciones que he considerado: -

  1. Utilice un JsonConverter personalizado, pero construya el JSON manualmente en lugar de aprovechar JToken.FromObject (agrega mucha complejidad)
  2. Usar un ActionFilterAttribute y eliminar propiedades del modelo antes de la serialización (necesitaría usar el reflejo para cada solicitud para modificar el objeto modelo)
  3. Usar métodos ShouldSerialzeX() en mis modelos que realizan búsquedas (no fácilmente mantenibles)
  4. Usar un ContractResolver personalizado (esto tiene el mismo problema de almacenamiento en caché, incluso si uso el constructor ahora obsoleto en DefaultContractResolver que establece "shareCache" en falso)

¿Alguien puede sugerir:

  • Una forma de hacer JsonConverters por solicitud
  • Suponiendo que no se puede hacer por solicitud, una forma de solucionar el problema de subprocesamiento con JsonConverter
  • Una alternativa a JsonConverter que me permite inspeccionar y modificar globalmente objetos JSON antes de que se devuelvan al cliente que no depende de una gran cantidad de reflexión
  • ¿Algo más?

Gracias de antemano por tomarse el tiempo para leer esto.


Una posibilidad para reparar el TestConverter para múltiples TestConverter de subprocesos múltiples sería crear una pila [ThreadStatic] de tipos que se serializaran. Luego, en CanConvert , devuelve false si el tipo candidato es del mismo tipo que el tipo en la parte superior de la pila.

Tenga en cuenta que esto solo funciona cuando el convertidor está incluido en JsonSerializerSettings.Converters . Si el convertidor se aplica directamente a una clase o propiedad con, por ejemplo,

[JsonConverter(typeof(TestConverter<Inua.WebApi.Authentication.IUser>))]

Entonces la recursión infinita seguirá ocurriendo ya que CanConvert no se llama para los convertidores aplicados directamente.

Así:

public class TestConverter<TBaseType> : JsonConverter { [ThreadStatic] static Stack<Type> typeStack; static Stack<Type> TypeStack { get { return typeStack = (typeStack ?? new Stack<Type>()); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { JToken token; using (TypeStack.PushUsing(value.GetType())) { token = JToken.FromObject(value, serializer); } // in practice this would be obtained dynamically string[] omit = new string[] { "Name" }; JObject jObject = token as JObject; foreach (JProperty property in jObject.Properties().Where(p => omit.Contains(p.Name, StringComparer.OrdinalIgnoreCase)).ToList()) { property.Remove(); } token.WriteTo(writer); } public override bool CanConvert(Type objectType) { if (typeof(TBaseType).IsAssignableFrom(objectType)) { return TypeStack.PeekOrDefault() != objectType; } return false; } public override bool CanRead { get { return false; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } } public static class StackExtensions { public struct PushValue<T> : IDisposable { readonly Stack<T> stack; public PushValue(T value, Stack<T> stack) { this.stack = stack; stack.Push(value); } #region IDisposable Members // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. public void Dispose() { if (stack != null) stack.Pop(); } #endregion } public static T PeekOrDefault<T>(this Stack<T> stack) { if (stack == null) throw new ArgumentNullException(); if (stack.Count == 0) return default(T); return stack.Peek(); } public static PushValue<T> PushUsing<T>(this Stack<T> stack, T value) { if (stack == null) throw new ArgumentNullException(); return new PushValue<T>(value, stack); } }

En su caso, TBaseType sería Inua.WebApi.Authentication.IUser .

Prototipo de violín .