问题
I want to reduce the string Json.NET generates by using default values.
One of my properties is the following:
public string Name { get; set; } = "Jennifer";
I already use the auto-property initializer so the string gets populated if empty.
When serializing with Json.NET I use DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate
so only changed properties get persisted.
I know that I can use the DefaultValueAttribut
like this:
[DefaultValue("Jennifer")]
public string Name { get; set; } = "Jennifer";
But I am wondering if I can skip this attribute and use auto-property initial values as default value when serializing.
回答1:
Yes, you can do this using a custom ContractResolver
such as the one below. It works by creating a reference instance for each Type
(assuming the Type
is a class and has a default constructor available) and then setting a ShouldSerialize
predicate on each property which checks the current value of the property against the reference instance. If they match, then ShouldSerialize
returns false and the property is not serialized.
class CustomResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
if (type.IsClass)
{
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
if (ctor != null)
{
object referenceInstance = ctor.Invoke(null);
foreach (JsonProperty prop in props.Where(p => p.Readable))
{
prop.ShouldSerialize = instance =>
{
object val = prop.ValueProvider.GetValue(instance);
object refVal = prop.ValueProvider.GetValue(referenceInstance);
return !ObjectEquals(val, refVal);
};
}
}
}
return props;
}
private bool ObjectEquals(object a, object b)
{
if (ReferenceEquals(a, b)) return true;
if (a == null || b == null) return false;
if (a is IEnumerable && b is IEnumerable && !(a is string) && !(b is string))
return EnumerableEquals((IEnumerable)a, (IEnumerable)b);
return a.Equals(b);
}
private bool EnumerableEquals(IEnumerable a, IEnumerable b)
{
IEnumerator enumeratorA = a.GetEnumerator();
IEnumerator enumeratorB = b.GetEnumerator();
bool hasNextA = enumeratorA.MoveNext();
bool hasNextB = enumeratorB.MoveNext();
while (hasNextA && hasNextB)
{
if (!ObjectEquals(enumeratorA.Current, enumeratorB.Current)) return false;
hasNextA = enumeratorA.MoveNext();
hasNextB = enumeratorB.MoveNext();
}
return !hasNextA && !hasNextB;
}
}
To use the resolver you need to add it to the JsonSerializerSettings
and pass the settings to the SerializeObject
method like this:
JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = new CustomResolver(),
};
string json = JsonConvert.SerializeObject(yourObject, settings);
Here is a working demo: https://dotnetfiddle.net/K1WbSP
Some notes on this solution:
Using this resolver has a functional advantage over
DefaultValue
attributes in that it can handle complex defaults likeLists
,Dictionaries
and child objects (provided you have correctly implemented theEquals
method on the child classes). Attributes can only handle simple constant expressions (e.g. strings, enums and other primitives). However, if all you need is simple defaults, be aware that this resolver will probably perform slightly worse than just using the attributes because it needs to use additional reflection to instantiate the reference objects and compare all the property values. So there is a little bit of a tradeoff. If your JSON is small, you probably won't notice a difference. But if your JSON is large, then you may want do some benchmarks.You may want to implement a class-level opt-out mechanism (i.e. a custom attribute that the resolver looks for, or maybe a list of class names that you pass to the resolver) in case you run into situations where you do want the default values to be serialized for certain classes but not others.
来源:https://stackoverflow.com/questions/51691331/json-net-get-default-values-from-auto-property-initializer