I\'m after a generic method that allows me to modify the JSON of an object being returned to the client, specifically the removal of certain properties in returned objects.
In typical fashion, the process of posing the question caused me to take a fresh take on the problem.
I have found one possible work-around: creating a custom MediaTypeFormatter.
With help from here and here, a potential solution:-
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();
}
});
}
}
}
and then configure the app to use it:-
// insert at 0 so it runs before System.Net.Http.Formatting.JsonMediaTypeFormatter
config.Formatters.Insert(0, new TestFormatter());
This creates a new instance of my JsonConverter for each request, which in combination with the other fixes in the original post, seem to solve the issue.
This is probably not the best way of doing this, so I'll leave this open for some better suggestions, or until I realise why this isn't going to work.
One possibility to fix the TestConverter
for multi-threaded, multi-type scenarios would be to create a [ThreadStatic]
stack of types being serialized. Then, in CanConvert, return false
if the candidate type is of the same type as the type on top of the stack.
Note this only works when the converter is included in JsonSerializerSettings.Converters. If the converter is applied directly to a class or property with, say,
[JsonConverter(typeof(TestConverter<Inua.WebApi.Authentication.IUser>))]
Then infinite recursion will still occur since CanConvert
is not called for directly applied converters.
Thus:
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);
}
}
In your case TBaseType
would be Inua.WebApi.Authentication.IUser
.
Prototype fiddle.