Ok, dopo aver scavato in giro fonte Json.NET per un po ', ho finalmente ottenuto questo lavoro e sarà anche onorare la ShouldSerialize * e * membri specificato che Json.NET supporta. State attenti: questo sta sicuramente andando nelle erbacce.
Così mi sono reso conto che la classe JsonProperty restituito da DefaultContractResolver.CreateProperty ha ShouldSerialize e convertitore di proprietà, che mi permettono di specificare se dovrebbe in realtà essere serializzato l'istanza di proprietà e, in caso affermativo, come farlo.
La deserializzazione richiede qualcosa di leggermente diverso. DefaultContractResolver.ResolveContract, per impostazione predefinita per un tipo personalizzato, restituisce un oggetto JsonObjectContract con una proprietà Convertitore nullo. Per deserializzare correttamente il mio tipo, avevo bisogno di impostare la proprietà Converter quando il contratto è per il mio tipo.
Ecco il codice (con la gestione degli errori/ecc rimosso per mantenere le cose il più piccolo possibile).
In primo luogo, il tipo che ha bisogno di un trattamento speciale:
public struct Optional<T>
{
public readonly bool ValueProvided;
public readonly T Value;
private Optional(T value)
{
this.ValueProvided = true;
this.Value = value;
}
public static implicit operator Optional<T>(T value)
{
return new Optional<T>(value);
}
}
E c'è il convertitore che serializzare correttamente dopo sappiamo che dovrebbe essere serializzato:
public class OptionalJsonConverter<T> : JsonConverter
{
public static OptionalJsonConverter<T> Instance = new OptionalJsonConverter<T>();
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var optional = (Optional<T>)value; // Cast so we can access the Optional<T> members
serializer.Serialize(writer, optional.Value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var valueType = objectType.GetGenericArguments()[ 0 ];
var innerValue = (T)serializer.Deserialize(reader, valueType);
return (Optional<T>)innerValue; // Explicitly invoke the conversion from T to Optional<T>
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Optional<T>);
}
}
Infine, e soprattutto -verbosamente, ecco il ContractResolver che inserisce i ganci:
public class CustomContractResolver : DefaultContractResolver
{
// For deserialization. Detect when the type is being deserialized and set the converter for it.
public override JsonContract ResolveContract(Type type)
{
var contract = base.ResolveContract(type);
if(contract.Converter == null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>))
{
// This may look fancy but it's just calling GetOptionalJsonConverter<T> with the correct T
var optionalValueType = type.GetGenericArguments()[ 0 ];
var genericMethod = this.GetAndMakeGenericMethod("GetOptionalJsonConverter", optionalValueType);
var converter = (JsonConverter)genericMethod.Invoke(null, null);
// Set the converter for the type
contract.Converter = converter;
}
return contract;
}
public static OptionalJsonConverter<T> GetOptionalJsonConverter<T>()
{
return OptionalJsonConverter<T>.Instance;
}
// For serialization. Detect when we're creating a JsonProperty for an Optional<T> member and modify it accordingly.
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var jsonProperty = base.CreateProperty(member, memberSerialization);
var type = jsonProperty.PropertyType;
if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Optional<>))
{
// This may look fancy but it's just calling SetJsonPropertyValuesForOptionalMember<T> with the correct T
var optionalValueType = type.GetGenericArguments()[ 0 ];
var genericMethod = this.GetAndMakeGenericMethod("SetJsonPropertyValuesForOptionalMember", optionalValueType);
genericMethod.Invoke(null, new object[]{ member.Name, jsonProperty });
}
return jsonProperty;
}
public static void SetJsonPropertyValuesForOptionalMember<T>(string memberName, JsonProperty jsonProperty)
{
if(jsonProperty.ShouldSerialize == null) // Honor ShouldSerialize*
{
jsonProperty.ShouldSerialize =
(declaringObject) =>
{
if(jsonProperty.GetIsSpecified != null && jsonProperty.GetIsSpecified(declaringObject)) // Honor *Specified
{
return true;
}
object optionalValue;
if(!TryGetPropertyValue(declaringObject, memberName, out optionalValue) &&
!TryGetFieldValue(declaringObject, memberName, out optionalValue))
{
throw new InvalidOperationException("Better error message here");
}
return ((Optional<T>)optionalValue).ValueProvided;
};
}
if(jsonProperty.Converter == null)
{
jsonProperty.Converter = CustomContractResolver.GetOptionalJsonConverter<T>();
}
}
// Utility methods used in this class
private MethodInfo GetAndMakeGenericMethod(string methodName, params Type[] typeArguments)
{
var method = this.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
return method.MakeGenericMethod(typeArguments);
}
private static bool TryGetPropertyValue(object declaringObject, string propertyName, out object value)
{
var propertyInfo = declaringObject.GetType().GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if(propertyInfo == null)
{
value = null;
return false;
}
value = propertyInfo.GetValue(declaringObject, BindingFlags.GetProperty, null, null, null);
return true;
}
private static bool TryGetFieldValue(object declaringObject, string fieldName, out object value)
{
var fieldInfo = declaringObject.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if(fieldInfo == null)
{
value = null;
return false;
}
value = fieldInfo.GetValue(declaringObject);
return true;
}
}
Spero che aiuti qualcun altro. Sentiti libero di fare domande se qualcosa non è chiaro o se sembra che mi sia sfuggito qualcosa.
Non sarebbe meglio assicurarsi che i dati che si stanno inserendo siano nel formato corretto per la serializzazione? Utilizzare in modo specifico un tipo o proiezione non standard in una query LINQ per ottenere i dati nel modo desiderato. –
@RickStrahl - Mi capita di lavorare con Amber. Il motivo per cui questo non funzionerà è che i campi da includere o escludere sono dinamici e parte del protocollo che stiamo usando. In altre parole, un campo che non è incluso nella serializzazione significa qualcosa di diverso dal campo che appare con un certo valore. –