我正在寻找一种通用方法,该方法允许我修改返回给客户端的对象的JSON,特别是删除返回对象中的某些属性。类似于此处的建议。
这些修改是不确定的,因为它们是根据与用户关联的规则根据每个请求确定的。因此,这不适合缓存的方法。
我已经回顾了几种方法。最明显的选择是 JsonConverter,但是这存在问题,如此处、此处和此处所列。
这种方法的主要问题是,在WriteJson
中调用JToken.FromObject
以获取特定值的 JSON,递归调用相同的 JsonConverter,从而导致循环。
我已经尝试了此处列出的解决方案的变体,它提供了一种暂时禁用CanWrite
以防止循环问题的方法。但是,它似乎不适用于多个并发请求。JsonConverter 的单个实例在不同时间更改和读取 CanWrite 属性状态的多个线程之间共享,从而导致结果不一致。
我还尝试在 WriteJson
中使用不同的序列化程序(即提供给该方法的序列化程序除外),但这不支持递归(因为该序列化程序不使用我的 JsonConverter),因此任何嵌套项目都不会由我的 JsonConverter 处理。从默认序列化程序的转换器集合中删除我的 JsonConverter 也有同样的问题。
基本上,如果我希望能够递归处理我的模型对象,我将遇到自引用循环问题。
理想情况下,JToken.FromObject
有某种方法可以选择性地不调用对象本身的 JsonConverter,但在序列化期间仍将其应用于任何子对象。只有当传递给CanConvert
的对象与传递给WriteJson
的最后一个对象不同时,我才通过修改CanConvert
将CanWrite
设置为 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();
}
}
}
我考虑过的选项:-
- 使用自定义 JsonConverter,但手动构建 JSON 而不是利用JToken.FromObject(增加了很多复杂性)
- 使用 ActionFilterAttribute 并从序列化之前的模型(我需要对每个请求修改模型对象)
- 在我的模型中使用
ShouldSerialzeX()
执行查找的方法(不容易维护) - 使用自定义合约解析程序(这遭受相同的缓存问题,即使我在将"shareCache"设置为false的默认合同解析器)
谁能建议:-
- 一种使 JsonConverters 按请求的方法
- 假设它不能按请求进行,这是一种使用 JsonConverter 解决线程问题的方法
- JsonConverter 的替代方案,允许我在将 JSON 对象返回到客户端之前对其进行全局检查和修改,这不依赖于大量的反射开销
- 别的?
提前感谢您抽出宝贵时间阅读本文。
修复多线程、多类型方案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
.
原型小提琴。
以典型的方式,提出问题的过程使我对问题有了新的认识。
我找到了一个可能的解决方法:创建自定义 MediaType格式化程序。
在这里和这里的帮助下,一个潜在的解决方案:-
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 的新实例,该实例与原始帖子中的其他修复程序相结合,似乎可以解决问题。
这可能不是最好的方法,所以我会保留这个开放以获得一些更好的建议,或者直到我意识到为什么这行不通。