我正在使用一个通用方法,它允许我修改返回给客户端的对象的JSON,特别是删除返回对象中的某些属性。与建议here类似。
这些修改是非确定性的,因为它们是根据与用户相关的规则按请求确定的。所以这不适合缓存的方法。
我已经回顾了几种方法。最明显的选择是JsonConverter,但是存在问题,如列出here,here和here。
这种方法的主要问题是在JToken.FromObject
中调用WriteJson
来获取特定值的JSON,递归调用相同的JsonConverter,从而产生循环。
我尝试了列出的解决方案here的变体,它提供了一种暂时禁用CanWrite
以防止循环问题的方法。但是,它似乎不适用于多个并发请求。 JsonConverter的单个实例在多个线程之间共享,这些线程在不同时间更改和读取CanWrite属性的状态,从而导致不一致的结果。
我也尝试在WriteJson
中使用不同的序列化程序(即不是提供给方法的序列化程序)但是这不支持递归(因为序列化程序不使用我的JsonConverter)所以任何嵌套我的JsonConverter不处理这些项目。从默认的序列化程序的转换器集合中删除我的JsonConverter也存在同样的问题。
基本上,如果我想能够递归处理我的模型对象,我将会遇到自引用循环问题。
理想情况下,JToken.FromObject
会有某种方式选择性地不在对象本身上调用JsonConverter,但在序列化期间仍然将它应用于任何子对象。只有当传递给CanConvert
的对象与传递给CanWrite
的最后一个对象的类型不同时,我才通过修改CanConvert
将WriteJson
设置为true来解决此问题。
然而,为了实现这一点,我需要一个每请求范围的JsonConverter(由于上面相同的线程原因),但我看不出如何获得它。
以下是我的例子: -
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();
}
}
}
我考虑的选项: -
ShouldSerialzeX()
方法执行查找(不易维护) 任何人都可以建议: -
提前感谢您花时间阅读本文。
答案 0 :(得分:2)
为多线程,多类型方案修复TestConverter
的一种可能性是创建一个序列化的[ThreadStatic]
类型的堆栈。然后,在CanConvert
中,如果候选类型与堆栈顶部的类型相同,则返回false
。
请注意,当转换器包含在JsonSerializerSettings.Converters
中时,此仅有效。如果转换器直接应用于类或属性,例如
[JsonConverter(typeof(TestConverter<Inua.WebApi.Authentication.IUser>))]
然后仍会发生无限递归,因为没有为直接应用的转换器调用CanConvert
。
因此:
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);
}
}
在您的情况下,TBaseType
将是Inua.WebApi.Authentication.IUser
。
原型fiddle。
答案 1 :(得分:1)
以典型的方式,提出这个问题的过程让我对问题采取了新的看法。
我找到了一种可能的解决方法:创建自定义MediaTypeFormatter。
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();
}
});
}
}
}
然后配置应用程序以使用它: -
// insert at 0 so it runs before System.Net.Http.Formatting.JsonMediaTypeFormatter
config.Formatters.Insert(0, new TestFormatter());
这为每个请求创建了一个新的JsonConverter实例,结合原始帖子中的其他修复,似乎解决了这个问题。
这可能不是最好的方法,所以我会留下这个更好的建议,或直到我意识到为什么这不起作用。