c# add property to interface dynamically - c#

I have an asp.net core webservice which needs to handle interface-properties of an object correctly (has to find out which implementation to use for deserialization of the json-string).
for that reason I've created an class which gets called from an interface-Attribute, like:
[JsonConverter(typeof(InterfaceJsonConverter<IInputFormat>))]
public interface IInputFormat
{
// ...
}
The model-class where the InterfaceJsonConverter is defined has to be .NET Standard 1.4 which doesn't Support assembly-loading.
But this technique is used to find out which assembly has an implementation of the Interface and which implementation is the closest to the json-object.
(I could of course add the Interfaces directly to the class instead of reading the assemblies but I want a dynamic-solution that I will never miss an implementation)
(originally I developed that in .net 4.6 and it worked great there).
So what I'm trying to do now is :
Create a new .NET Standard 1.6 Library
Put the InterfaceJsonConverter in that library
Remove the Attributes from the Interface(s)
Add Interface-Attributes at runtime...
Is there any way to archive this?
Or are there better ways to handle Interfaces in asp.net core?
just for completeness I will post the Code for InterfaceJsonConverter
public class InterfaceJsonConverter<T> : Newtonsoft.Json.JsonConverter
{
public InterfaceJsonConverter()
{
this.DerivedTypes = GetTypesOfImplementedInterfaces(typeof(T));
}
readonly HashSet<Type> derivedTypes = new HashSet<Type>();
public InterfaceJsonConverter(params Type[] types)
{
this.DerivedTypes = types;
}
public InterfaceJsonConverter(System.Reflection.Assembly assembly, string #namespace)
{
this.DerivedTypes = GetTypesInNamespace(assembly, #namespace);
}
private Type[] GetTypesOfImplementedInterfaces(Type type)
{
var parts = type.AssemblyQualifiedName.Split(',');
var assemblyName = parts[1].Trim();
var assemblies = AssemblyRepo.GetReferencingAssemblies(assemblyName);
//var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var types = new List<Type>();
foreach (var a in assemblies)
{
try
{
//type.GetInterfaces().Any(i => i.FullName == typeof(T).FullName)
var currentTypes = a.GetTypes().Where(t => t.GetTypeInfo().IsAbstract==false && t.GetInterfaces().Any(i => i.FullName == type.FullName)).ToList();
types.AddRange(currentTypes);
}
catch (System.Exception) { }// ignored}
}
if(types.Count==0)
throw new System.Exception("No class found which implements interface [" + typeof(T) + "].");
return types.ToArray();
}
private Type[] GetTypesInNamespace(System.Reflection.Assembly assembly, string #namespace)
{
//return assembly.GetTypes().Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray();
return assembly.GetTypes()
.Where(
type => String.Equals(type.Namespace, #namespace)
&& type.GetInterfaces().Any(i => i.FullName == typeof (T).FullName)
).ToArray();
}
public IEnumerable<Type> DerivedTypes
{
get
{
return derivedTypes.ToArray();
}
set
{
if (value == null)
throw new ArgumentNullException();
derivedTypes.Clear();
derivedTypes.UnionWith(value);
}
}
JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
{
List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
foreach (var type in derivedTypes)
{
if (type.GetTypeInfo().IsAbstract)
continue;
var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
if (contract == null)
continue;
if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
continue;
if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
{
bestContracts.Clear();
bestContracts.Add(contract);
}
else if (contract.Properties.Count == bestContracts[0].Properties.Count)
{
bestContracts.Add(contract);
}
}
return bestContracts.Count > 0 ? bestContracts.Single() : null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None)
{
// Prevent infinite recursion when using an explicit converter in the list.
var removed = serializer.Converters.Remove(this);
try
{
// Kludge to prevent infinite recursion when using JsonConverterAttribute on the type: deserialize to object.
return obj.ToObject(typeof (object), serializer);
}
finally
{
if (removed)
serializer.Converters.Add(this);
}
}
var contract = FindContract(obj, serializer);
if (contract == null)
throw new JsonSerializationException("no contract found for " + obj.ToString());
if (existingValue == null || !contract.UnderlyingType.IsInstanceOfType(existingValue))
//!contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
existingValue = contract.DefaultCreator();
//will call the default constructor(PARAMETERLESS)...MAKE SURE YOUR CLASS HAS ONE!!!!!
using (var sr = obj.CreateReader())
{
serializer.Populate(sr, existingValue);
}
return existingValue;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Related

ASP.NET core controller serialize DateTime without time

I would like to format output from DateTime when there is no time part as yyyy-MM-dd i.s.o. yyyy-MM-ddT00:00:00 default in my case.
The controller API is :
[HttpGet("date")]
public async Task<DateTime> GetDateAsync(Date)
{
// dummy code
return DateTime.Now.Date;
}
In Swagger the output is seen as yyyy-MM-ddT00:00:00
I suppose this is due to
internal const string DefaultDateFormatString = #"yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; in Newtonsoft.Json.JsonSerializerSettings
But I want it to be like yyyy-MM-dd where the time part is not there:
if (value is DateTime d)
{
if (d.TotalSeconds == 0)
{
writer.WriteValue(d.ToString("yyyy-MM-dd"));
}
}
So I would like to know how to customize the serialization under the above condition and then configure that at startup.
This only for data output from the controllers.
I am using ASP.NET 3.1 and using Newtonsoft.Json
Thanks
I created a converter and hooked it up at start-up:
//...
AddNewtonsoftJson(options =>
{
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new JsonDateOnlyConverter());
})
public class JsonDateOnlyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(DateTime) || objectType == typeof(DateTime?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
var t = token.ToString();
if (token.Type == JTokenType.Date )
{
return token.ToObject<DateTime>();
}
if (token.Type == JTokenType.String)
{
if (string.IsNullOrEmpty(t) || string.IsNullOrWhiteSpace(t))
{
return null;
}
if (DateTime.TryParse(t, out var val))
{
return val;
}
}
if (token.Type == JTokenType.Null && (objectType == typeof(DateTime?) || objectType == typeof(DateTime)))
{
return null;
}
throw new JsonSerializationException("Unexpected token type: " + t);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateTime d)
{
if (d.Date.Ticks == d.Ticks)
{
writer.WriteValue(d.ToString("yyyy-MM-dd"));
}
}
}
}

.Net Core 3.0 JsonSerializer populate existing object

I'm preparing a migration from ASP.NET Core 2.2 to 3.0.
As I don't use more advanced JSON features (but maybe one as described below), and 3.0 now comes with a built-in namespace/classes for JSON, System.Text.Json, I decided to see if I could drop the previous default Newtonsoft.Json.
Do note, I'm aware that System.Text.Json will not completely replace Newtonsoft.Json.
I managed to do that everywhere, e.g.
var obj = JsonSerializer.Parse<T>(jsonstring);
var jsonstring = JsonSerializer.ToString(obj);
but in one place, where I populate an existing object.
With Newtonsoft.Json one can do
JsonConvert.PopulateObject(jsonstring, obj);
The built-in System.Text.Json namespace has some additional classes, like JsonDocumnet, JsonElement and Utf8JsonReader, though I can't find any that take an existing object as a parameter.
Nor am I experienced enough to see how to make use of the existing one's.
There might be a possible upcoming feature in .Net Core (thanks to Mustafa Gursel for the link), but meanwhile (and what if it doesn't),...
...I now wonder, is it possible to achieve something similar as what one can do with PopulateObject?
I mean, is it possible with any of the other System.Text.Json classes to accomplish the same, and update/replace only the properties set?,... or some other clever workaround?
Here is a sample input/output of what I am looking for, and it need to be generic as the object passed into the deserialization method is of type <T>). I have 2 Json string's to be parsed into an object, where the first have some default properties set, and the second some, e.g.
Note, a property value can be of any other type than a string.
Json string 1:
{
"Title": "Startpage",
"Link": "/index",
}
Json string 2:
{
"Head": "Latest news"
"Link": "/news"
}
Using the 2 Json strings above, I want an object resulting in:
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
As seen in above sample, if properties in the 2nd has values/is set, it replace values in the 1st (as with "Head" and "Link"), if not, existing value persist (as with "Title")
So assuming that Core 3 doesn't support this out of the box, let's try to work around this thing. So, what's our problem?
We want a method that overwrites some properties of an existing object with the ones from a json string. So our method will have a signature of:
void PopulateObject<T>(T target, string jsonSource) where T : class
We don't really want any custom parsing as it's cumbersome, so we'll try the obvious approach - deserialize jsonSource and copy the result properties into our object. We cannot, however, just go
T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);
That's because for a type
class Example
{
int Id { get; set; }
int Value { get; set; }
}
and a JSON
{
"Id": 42
}
we will get updateObject.Value == 0. Now we don't know if 0 is the new updated value or if it just wasn't updated, so we need to know exactly which properties jsonSource contains.
Fortunately, the System.Text.Json API allows us to examine the structure of the parsed JSON.
using var json = JsonDocument.Parse(jsonSource).RootElement;
We can now enumerate over all properties and copy them.
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
We will copy the value using reflection:
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
We can see here that what we're doing is a shallow update. If the object contains another complex object as its property, that one will be copied and overwritten as a whole, not updated. If you require deep updates, this method needs to be changed to extract the current value of the property and then call the PopulateObject recursively if the property's type is a reference type (that will also require accepting Type as a parameter in PopulateObject).
Joining it all together we get:
void PopulateObject<T>(T target, string jsonSource) where T : class
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
}
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
How robust is this? Well, it certainly won't do anything sensible for a JSON array, but I'm not sure how you'd expect a PopulateObject method to work on an array to begin with. I don't know how it compares in performance to the Json.Net version, you'd have to test that by yourself. It also silently ignores properties that are not in the target type, by design. I thought it was the most sensible approach, but you might think otherwise, in that case the property null-check has to be replaced with an exception throw.
EDIT:
I went ahead and implemented a deep copy:
void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
void PopulateObject(object target, string jsonSource, Type type)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property, type);
}
}
void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
To make this more robust you'd either have to have a separate PopulateObjectDeep method or pass PopulateObjectOptions or something similar with a deep/shallow flag.
EDIT 2:
The point of deep-copying is so that if we have an object
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 32
},
"Value": 128
}
and populate it with
{
"Child":
{
"Value": 64
}
}
we'd get
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 64
},
"Value": 128
}
In case of a shallow copy we'd get Id = 0 in the copied child.
EDIT 3:
As #ldam pointed out, this no longer works in stable .NET Core 3.0, because the API was changed. The Parse method is now Deserialize and you have to dig deeper to get to a JsonElement's value. There is an active issue in the corefx repo to allow direct deserialization of a JsonElement. Right now the closest solution is to use GetRawText(). I went ahead and edited the code above to work, leaving the old version struck-through.
Here is some sample code that does it. It's using the new Utf8JsonReader struct so it populates the object at the same time it parses it. It supports JSON/CLR types equivalence, nested objects (creates if they don't exist), lists and arrays.
var populator = new JsonPopulator();
var obj = new MyClass();
populator.PopulateObject(obj, "{\"Title\":\"Startpage\",\"Link\":\"/index\"}");
populator.PopulateObject(obj, "{\"Head\":\"Latest news\",\"Link\":\"/news\"}");
public class MyClass
{
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
}
Note it doesn't support all of what you would probably expect, but you can override or customize it. Things that could be added: 1) naming convention. You'd have to override the GetProperty method. 2) dictionaries or expando objects. 3) performance can be improved because it uses Reflection instead of MemberAccessor/delegate techniques
public class JsonPopulator
{
public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
{
options ??= new JsonSerializerOptions();
var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
new Worker(this, reader, obj, options);
}
protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = obj.GetType().GetProperty(propertyName);
return prop;
}
protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
if (propertyName == null)
throw new ArgumentNullException(nameof(propertyName));
var prop = GetProperty(ref reader, options, obj, propertyName);
if (prop == null)
return false;
if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
return false;
prop.SetValue(obj, value);
return true;
}
protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (reader.TokenType == JsonTokenType.Null)
{
value = null;
return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
}
if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
if (propertyType == typeof(string)) { value = JsonSerializer.Deserialize<JsonElement>(ref reader, options).GetString(); return true; }
if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }
if (propertyType == typeof(bool))
{
if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
{
value = reader.GetBoolean();
return true;
}
}
// fallback here
return TryConvertValue(ref reader, propertyType, out value);
}
protected virtual object ReadValue(ref Utf8JsonReader reader)
{
switch (reader.TokenType)
{
case JsonTokenType.False: return false;
case JsonTokenType.True: return true;
case JsonTokenType.Null: return null;
case JsonTokenType.String: return reader.GetString();
case JsonTokenType.Number: // is there a better way?
if (reader.TryGetInt32(out var i32))
return i32;
if (reader.TryGetInt64(out var i64))
return i64;
if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
return ui64;
if (reader.TryGetSingle(out var sgl))
return sgl;
if (reader.TryGetDouble(out var dbl))
return dbl;
if (reader.TryGetDecimal(out var dec))
return dec;
break;
}
throw new NotSupportedException();
}
// we're here when json types & property types don't match exactly
protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
{
if (propertyType == null)
throw new ArgumentNullException(nameof(reader));
if (propertyType == typeof(bool))
{
if (reader.TryGetInt64(out var i64)) // one size fits all
{
value = i64 != 0;
return true;
}
}
// TODO: add other conversions
value = null;
return false;
}
protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
{
if (propertyType.GetConstructor(Type.EmptyTypes) == null)
return null;
// TODO: handle custom instance creation
try
{
return Activator.CreateInstance(propertyType);
}
catch
{
// swallow
return null;
}
}
private class Worker
{
private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
private readonly Stack<object> _objects = new Stack<object>();
public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
{
_objects.Push(obj);
WorkerProperty prop;
WorkerProperty peek;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
prop = new WorkerProperty();
prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
_properties.Push(prop);
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
if (_properties.Count > 0)
{
object child = null;
var parent = _objects.Peek();
PropertyInfo pi = null;
if (parent != null)
{
pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
if (pi != null)
{
child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
if (child == null && pi.CanWrite)
{
if (reader.TokenType == JsonTokenType.StartArray)
{
if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
break; // don't create if we can't handle it
}
if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
{
child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
}
else
{
child = populator.CreateInstance(ref reader, pi.PropertyType);
if (child != null)
{
pi.SetValue(parent, child);
}
}
}
}
}
if (reader.TokenType == JsonTokenType.StartObject)
{
_objects.Push(child);
}
else if (child != null) // StartArray
{
peek = _properties.Peek();
peek.IsArray = pi.PropertyType.IsArray;
peek.List = (IList)child;
peek.ListPropertyType = GetListElementType(child.GetType());
peek.ArrayPropertyInfo = pi;
}
}
break;
case JsonTokenType.EndObject:
_objects.Pop();
if (_properties.Count > 0)
{
_properties.Pop();
}
break;
case JsonTokenType.EndArray:
if (_properties.Count > 0)
{
prop = _properties.Pop();
if (prop.IsArray)
{
var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
prop.List.CopyTo(array, 0);
prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
}
}
break;
case JsonTokenType.False:
case JsonTokenType.Null:
case JsonTokenType.Number:
case JsonTokenType.String:
case JsonTokenType.True:
peek = _properties.Peek();
if (peek.List != null)
{
if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
{
peek.List.Add(item);
}
break;
}
prop = _properties.Pop();
var current = _objects.Peek();
if (current != null)
{
populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
}
break;
}
}
}
private static Type GetListElementType(Type type)
{
if (type.IsArray)
return type.GetElementType();
foreach (Type iface in type.GetInterfaces())
{
if (!iface.IsGenericType) continue;
if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
}
return typeof(object);
}
}
private class WorkerProperty
{
public string PropertyName;
public IList List;
public Type ListPropertyType;
public bool IsArray;
public PropertyInfo ArrayPropertyInfo;
public override string ToString() => PropertyName;
}
}
The workaround can also be as simple as this (supports multi-level JSON as well):
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
namespace ConsoleApp
{
public class Model
{
public Model()
{
SubModel = new SubModel();
}
public string Title { get; set; }
public string Head { get; set; }
public string Link { get; set; }
public SubModel SubModel { get; set; }
}
public class SubModel
{
public string Name { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
var model = new Model();
Console.WriteLine(JsonSerializer.ToString(model));
var json1 = "{ \"Title\": \"Startpage\", \"Link\": \"/index\" }";
model = Map<Model>(model, json1);
Console.WriteLine(JsonSerializer.ToString(model));
var json2 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Name\": \"Reyan Chougle\" } }";
model = Map<Model>(model, json2);
Console.WriteLine(JsonSerializer.ToString(model));
var json3 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Engineer\" } }";
model = Map<Model>(model, json3);
Console.WriteLine(JsonSerializer.ToString(model));
var json4 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\", \"SubModel\": { \"Description\": \"I am a Software Programmer\" } }";
model = Map<Model>(model, json4);
Console.WriteLine(JsonSerializer.ToString(model));
Console.ReadKey();
}
public static T Map<T>(T obj, string jsonString) where T : class
{
var newObj = JsonSerializer.Parse<T>(jsonString);
foreach (var property in newObj.GetType().GetProperties())
{
if (obj.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(newObj) != null))
{
if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == typeof(T).Assembly.FullName)
{
MethodInfo mapMethod = typeof(Program).GetMethod("Map");
MethodInfo genericMethod = mapMethod.MakeGenericMethod(property.GetValue(newObj).GetType());
var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(newObj), JsonSerializer.ToString(property.GetValue(newObj)) });
foreach (var property2 in obj2.GetType().GetProperties())
{
if (property2.GetValue(obj2) != null)
{
property.GetValue(obj).GetType().GetProperty(property2.Name).SetValue(property.GetValue(obj), property2.GetValue(obj2));
}
}
}
else
{
property.SetValue(obj, property.GetValue(newObj));
}
}
}
return obj;
}
}
}
Output:
I do not know much about this new version of the plug-in, however I found a tutorial that can be followed tutorial with some examples
Based on him I thought of this method and I imagine that he is able to solve his problem
//To populate an existing variable we will do so, we will create a variable with the pre existing data
object PrevData = YourVariableData;
//After this we will map the json received
var NewObj = JsonSerializer.Parse<T>(jsonstring);
CopyValues(NewObj, PrevData)
//I found a function that does what you need, you can use it
//source: https://stackoverflow.com/questions/8702603/merging-two-objects-in-c-sharp
public void CopyValues<T>(T target, T source)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (source== null) throw new ArgumentNullException(nameof(source));
Type t = typeof(T);
var properties = t.GetProperties(
BindingFlags.Instance | BindingFlags.Public).Where(prop =>
prop.CanRead
&& prop.CanWrite
&& prop.GetIndexParameters().Length == 0);
foreach (var prop in properties)
{
var value = prop.GetValue(source, null);
prop.SetValue(target, value, null);
}
}
This code is based on the answer given by V0ldek.
It adds the use of custom converters if they are defined on properties.
Only properties with public Setter are updated.
/// <summary>
/// Utility class for System.Text.Json
/// </summary>
public static class JsonUtility
{
/// <summary>
/// Update an objet from JSON data
/// </summary>
/// <param name="type">Type of the object to update</param>
/// <param name="target">Object to update</param>
/// <param name="jsonSource">JSON Data</param>
/// <remarks>This code is based on the answer given by V0ldek on StackOverflow</remarks>
/// <see cref="https://stackoverflow.com/a/56906228/3216022"/>
public static void PopulateObject(Type type, object target, string jsonSource, JsonSerializerOptions options)
{
var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
OverwriteProperty(property);
void OverwriteProperty(JsonProperty updatedProperty)
{
var propertyInfo = type.GetProperty(updatedProperty.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (!(propertyInfo?.SetMethod?.IsPublic).GetValueOrDefault())
return;
if (propertyInfo.GetCustomAttribute<JsonIgnoreAttribute>() != null)
return;
// If the property has a Converter attribute, we use it
var converter = GetJsonConverter(propertyInfo);
if (converter != null)
{
var serializerOptions = new JsonSerializerOptions(options);
serializerOptions.Converters.Add(converter);
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, serializerOptions);
propertyInfo.SetValue(target, parsedValue);
}
else
{
var parsedValue = JsonSerializer.Deserialize(updatedProperty.Value.GetRawText(), propertyInfo.PropertyType, options);
propertyInfo.SetValue(target, parsedValue);
}
}
}
/// <summary>
/// Return the JSON Converter of a property (null if not exists)
/// </summary>
/// <param name="propertyInfo">Property</param>
/// <see cref="https://github.com/dotnet/runtime/blob/v6.0.3/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs"/>
public static JsonConverter GetJsonConverter(PropertyInfo propertyInfo)
{
var attribute = propertyInfo.GetCustomAttribute<JsonConverterAttribute>();
if (attribute != null)
{
if (attribute.ConverterType == null)
return attribute.CreateConverter(propertyInfo.PropertyType);
else
{
var ctor = attribute.ConverterType.GetConstructor(Type.EmptyTypes);
if (typeof(JsonConverter).IsAssignableFrom(attribute.ConverterType) && (ctor?.IsPublic).GetValueOrDefault())
return (JsonConverter)Activator.CreateInstance(attribute.ConverterType)!;
}
}
return null;
}
}
If you already use AutoMapper in your project or don't mind having dependency on it, you can merge objects in a following way:
var configuration = new MapperConfiguration(cfg => cfg
.CreateMap<Model, Model>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != default)));
var mapper = configuration.CreateMapper();
var destination = new Model {Title = "Startpage", Link = "/index"};
var source = new Model {Head = "Latest news", Link = "/news"};
mapper.Map(source, destination);
class Model
{
public string Head { get; set; }
public string Title { get; set; }
public string Link { get; set; }
}
I am not sure if this will fix your problem, but it should work as a temporary workaround. All I did was write a simple class with a populateobject method in it.
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
I put it into a console app for testing purposes. Below is the entire app if you would like to test it yourself.
using System;
using System.Text.Json;
using System.IO;
using System.Text.Json.Serialization;
namespace JsonQuestion1
{
class Program
{
static void Main(string[] args)
{
// Only used for testing
string path = #"C:\Users\Path\To\JsonFiles";
string st1 = File.ReadAllText(path + #"\st1.json");
string st2 = File.ReadAllText(path + #"\st2.json");
// Only used for testing ^^^
string myObject = MyDeserializer.PopulateObject(new[] { st1, st2 } );
Console.WriteLine(myObject);
Console.ReadLine();
}
}
public class MyDeserializer
{
public static string PopulateObject(string[] jsonStrings)
{
Dictionary<string, object> fullEntity = new Dictionary<string, object>();
if (jsonStrings != null && jsonStrings.Length > 0)
{
for (int i = 0; i < jsonStrings.Length; i++)
{
var myEntity = JsonSerializer.Parse<Dictionary<string, object>>(jsonStrings[i]);
foreach (var key in myEntity.Keys)
{
if (!fullEntity.ContainsKey(key))
{
fullEntity.Add(key, myEntity[key]);
}
else
{
fullEntity[key] = myEntity[key];
}
}
}
}
return JsonSerializer.ToString(fullEntity);
}
}
}
Json File Contents:
st1.json
{
"Title": "Startpage",
"Link": "/index"
}
st2.json
{
"Title": "Startpage",
"Head": "Latest news",
"Link": "/news"
}
If its just one usage and you don't want to add extra dependencies / lots of code, you don't mind a bit of inefficiency and I've not missed something obvious, you can just use:
private static T ParseWithTemplate<T>(T template, string input)
{
var ignoreNulls = new JsonSerializerOptions() { IgnoreNullValues = true };
var templateJson = JsonSerializer.ToString(template, ignoreNulls);
var combinedData = templateJson.TrimEnd('}') + "," + input.TrimStart().TrimStart('{');
return JsonSerializer.Parse<T>(combinedData);
}

JsonConverter: Return a single object or a List<Object> based on inbound JSON

When my API sends back a single object, we'll call it Item (A GetEntity in OData terms), it looks like this:
{
"d" : {
"Item" : "123456",
"OldItem" : "78921",
}
}
When I grab a set of the same object, i.e. returning List of Item, I get:
{
"d":{
"results":[
{
"Item":"343431",
"OldItem":"21314"
},
{
"Item":"341321",
"OldItem":"43563"
}
]
}
}
Other than the obvious "d" base node I need to get rid of, I'm having trouble attempting to use the same class in C# to do this. I have an Item class as such:
public class Material : IEntity
{
[JsonProperty("Item")]
public string material_number { get; set; }
[JsonProperty("OldItem")]
public string old_material_number { get; set; }
// Methods
public Material() {}
public bool Validate() { throw new NotImplementedException(); }
}
I'd like to be able to call a custom JsonConverter to handle this, but I haven't been able to get some of the example out there for single object, array converters to work. Ideally, I should be able to call:
JsonConvert.DeserializeObject<T> where T is either Material or List<Material>. How can I build a JsonConverter to handle both scenarios?
I'm calling the JsonConvert.DeserializeObject<T> as such:
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<T>(JObject.Parse(response.Content.ReadAsStringAsync().Result).SelectToken("d").ToString());
}
else
{
throw new Exception("Service Error");
}
Here is a JsonConverter that should work for your situation. Note that if there are any other JSON formats you could receive that are not shown in your question -- for example, if d can have a value of null when there are no results -- you may need to make adjustments to the converter. Currently it will throw an exception if it encounters something it does not expect, but you could make it return null or an empty list instead, if you prefer.
public class MaterialArrayConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
JToken results = token["results"];
if (results != null && results.Type == JTokenType.Array)
{
// we've got multiple items; deserialize to a list
return results.ToObject<List<Material>>(serializer);
}
else if (results == null)
{
// "results" property not present; return a list of one item
return new List<Material> { token.ToObject<Material>(serializer) };
}
}
// some other format we're not expecting
throw new JsonSerializationException("Unexpected JSON format encountered in MaterialArrayConverter: " + token.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanWrite
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
}
You can use it by making a RootObject class annotated as shown below and then deserializing your JSON into that:
public class RootObject
{
[JsonProperty("d")]
[JsonConverter(typeof(MaterialArrayConverter))]
public List<Material> Materials { get; set; }
}
Then:
var root = JsonConvert.DeserializeObject<RootObject>(json);
From there you can retrieve the list of materials and use it as you see fit.
Fiddle: https://dotnetfiddle.net/tKb6Ke
Assuming you want to return your material(s) in a collection, you can use the following generic JsonConverter
public class SingleOrResultListConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(ICollection<T>).IsAssignableFrom(objectType);
}
const string Results = "results";
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (objectType.IsArray)
{
var list = (List<T>)ReadJson(reader, typeof(List<T>), new List<T>(), serializer);
return list.ToArray();
}
else
{
var list = (ICollection<T>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
{
serializer.Populate(reader, list);
}
else if (reader.TokenType == JsonToken.StartObject)
{
JObject obj = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value.ToString();
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected end while reading collection");
}
if (propertyName == Results)
{
serializer.Populate(reader, list);
}
else
{
obj = obj ?? new JObject();
obj[propertyName] = JToken.Load(reader);
}
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
if (obj != null)
list.Add(obj.ToObject<T>(serializer));
return list;
}
}
throw new JsonSerializationException("Unexpected end while reading collection");
}
else
{
throw new JsonSerializationException("Unexpected start token: " + reader.TokenType);
}
return list;
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var collection = (ICollection<T>)value;
if (collection.Count == 1)
{
serializer.Serialize(writer, collection.First());
}
else
{
writer.WriteStartObject();
writer.WritePropertyName(Results);
writer.WriteStartArray();
foreach (var item in collection)
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
}
Then use it as follows:
var obj = JObject.Parse(json);
var subObj = obj["d"] ?? obj; // Strip the "d".
var settings = new JsonSerializerSettings { Converters = new[] { new SingleOrResultListConverter<Material>() } };
var list = subObj.ToObject<List<Material>>(JsonSerializer.CreateDefault(settings));
Or if you prefer an array to a List<T>:
var array = subObj.ToObject<Material[]>(JsonSerializer.CreateDefault(settings));
Prototype fiddle.
Update
You need to make sure you are allocating and passing your converter to Json.NET. Since your deserialization method is generic, you'll need to do something generic, like so:
var json = response.Content.ReadAsStringAsync().Result;
var converters = typeof(T).GetCollectionItemTypes()
.Select(t => (JsonConverter)Activator.CreateInstance(typeof(SingleOrResultListConverter<>).MakeGenericType(new [] { t })))
.ToArray();
var settings = new JsonSerializerSettings { Converters = converters };
return JToken.Parse(json).SelectToken("d").ToObject<T>(JsonSerializer.CreateDefault(settings));
Using the extension method:
public static class TypeExtensions
{
/// <summary>
/// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetCollectionItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}

Json.Net PopulateObject - update list elements based on ID

It is possible to define a custom "list-merge" startegy used for the JsonConvert.PopulateObject method?
Example:
I have two models:
class Parent
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public List<Child> Childs { get; set; }
}
class Child
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
My initial JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Name":"John",
"Childs":[
{
"Uuid":"96b93f95-9ce9-441d-bfb0-f44b65f7fe0d",
"Name":"Philip",
"Score":100
},
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Name":"Peter",
"Score":150
},
{
"Uuid":"1d2cdba4-9efb-44fc-a2f3-6b86a5291954",
"Name":"Steve",
"Score":80
}
]
}
and my update JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Childs":[
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Score":170
}
]
}
All I need is to specify a model property (by attribute) used for matching list items (in my case the Uuid property of Child), so calling the JsonConvert.PopulateObject on the object deserialized from my initial JSON with a update JSON (it contains ONLY changed values + Uuids for every object) results to update only list elements contained in the update JSON macthed by Uuid (in my case update a Peter's score) and elements not contained in the update JSON leave without change.
I'm searching for some universal solution - I need to apply it on large JSONs with a lot of nested lists (but every model has some unique property). So I need to recursively call PopulateObject on matched list item.
You could create your own JsonConverter that implements the required merge logic. This is possible because JsonConverter.ReadJson is passed an existingValue parameter that contains the pre-existing contents of the property being deserialized.
Thus:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMergeKeyAttribute : System.Attribute
{
}
public class KeyedListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
elementType = objectType.GetListType();
if (elementType == null)
{
keyProperty = null;
return false;
}
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var list = existingValue as IList;
if (list == null || list.Count == 0)
{
list = list ?? (IList)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject(elementType, serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
if (object.ReferenceEquals(x, y))
return true;
else if (x == null || y == null)
return false;
return x.Equals(y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
public static class TypeExtensions
{
public static Type GetListType(this Type type)
{
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
Notice that the converter needs to know the IContractResolver currently in use. Having it makes finding the key parameter easier, and also ensures that, if the key parameter has a [JsonProperty(name)] attribute, the replacement name is respected.
Then add the attribute:
class Child
{
[JsonMergeKey]
[JsonProperty("Uuid")] // Replacement name for testing
public Guid UUID { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
And use the converter as follows:
var serializer = JsonSerializer.CreateDefault();
var converter = new KeyedListMergeConverter(serializer.ContractResolver);
serializer.Converters.Add(converter);
using (var reader = new StringReader(updateJson))
{
serializer.Populate(reader, parent);
}
The converter assumes that the key parameter is always present in the JSON. Also, if any entries in the JSON being merged have keys that are not found in the existing list, they are appended to the list.
Update
The original converter is specifically hardcoded for List<T>, and takes advantage of the fact that List<T> implements both IList<T> and IList. If your collection is not a List<T> but still implements IList<T>, the following should work:
public class KeyedIListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedIListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
if (objectType.IsArray)
{
// Not implemented for arrays, since they cannot be resized.
elementType = null;
keyProperty = null;
return false;
}
var elementTypes = objectType.GetIListItemTypes().ToList();
if (elementTypes.Count != 1)
{
elementType = null;
keyProperty = null;
return false;
}
elementType = elementTypes[0];
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { elementType });
try
{
return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer, keyProperty });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("ReadJsonGeneric<T> error", ex);
}
}
object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty)
{
var list = existingValue as IList<T>;
if (list == null || list.Count == 0)
{
list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject<T>(serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
return object.Equals(x, y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetIListItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IList<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
Note that merging is not implemented for arrays since they are not resizable.

Strategies for migrating serialized Json.NET document between versions/formats

I'm using Json.Net to serialize some application data. Of course, the application specs have slightly changed and we need to refactor some of the business object data. What are some viable strategies to migrate previously serialized data to our new data format?
For example, say we have orignally had a business object like:
public class Owner
{
public string Name {get;set;}
}
public class LeaseInstrument
{
public ObservableCollection<Owner> OriginalLessees {get;set;}
}
We serialize an instance of a LeaseInstrument to a file with Json.Net. Now, we change our business objects to look like:
public class Owner
{
public string Name {get;set;}
}
public class LeaseOwner
{
public Owner Owner { get;set;}
public string DocumentName {get;set;}
}
public class LeaseInstrument
{
public ObservableCollection<LeaseOwner> OriginalLessees {get;set;}
}
I have looked into writing a custom JsonConverter for LeaseInstrument, but the ReadJson method is not ever hit...instead an exception is thrown before the deserializer reaches that point:
Additional information: Type specified in JSON
'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.Owner,
BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
is not compatible with 'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.LeaseOwner, BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'. Path 'Is.$values[8].OriginalLessors.$type', line 3142, position 120.
I mean, no joke, Json.Net, that's why I'm trying to run a JsonConverter when deserializing these objects, so I can manually handle the fact that the serialized type doesn't match the compiled type!!
For what it's worth, here are the JsonSerializerSettings we are using:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new WritablePropertiesOnlyResolver(),
TypeNameHandling = TypeNameHandling.All,
ObjectCreationHandling = ObjectCreationHandling.Reuse
};
You have the following issues:
You serialized using TypeNameHandling.All. This setting serializes type information for collections as well as objects. I don't recommend doing this. Instead I suggest using TypeNameHandling.Objects and then letting the deserializing system choose the collection type.
That being said, to deal with your existing JSON, you can adapt the IgnoreArrayTypeConverter from make Json.NET ignore $type if it's incompatible to use with a resizable collection:
public class IgnoreCollectionTypeConverter : JsonConverter
{
public IgnoreCollectionTypeConverter() { }
public IgnoreCollectionTypeConverter(Type ItemConverterType)
{
this.ItemConverterType = ItemConverterType;
}
public Type ItemConverterType { get; set; }
public override bool CanConvert(Type objectType)
{
// TODO: test with read-only collections.
return objectType.GetCollectItemTypes().Count() == 1 && !objectType.IsDictionary() && !objectType.IsArray;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!CanConvert(objectType))
throw new JsonSerializationException(string.Format("Invalid type \"{0}\"", objectType));
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
var itemConverter = (ItemConverterType == null ? null : (JsonConverter)Activator.CreateInstance(ItemConverterType, true));
if (itemConverter != null)
serializer.Converters.Add(itemConverter);
try
{
return ToCollection(token, objectType, existingValue, serializer);
}
finally
{
if (itemConverter != null)
serializer.Converters.RemoveLast(itemConverter);
}
}
private static object ToCollection(JToken token, Type collectionType, object existingValue, JsonSerializer serializer)
{
if (token == null || token.Type == JTokenType.Null)
return null;
else if (token.Type == JTokenType.Array)
{
// Here we assume that existingValue already is of the correct type, if non-null.
existingValue = serializer.DefaultCreate<object>(collectionType, existingValue);
token.PopulateObject(existingValue, serializer);
return existingValue;
}
else if (token.Type == JTokenType.Object)
{
var values = token["$values"];
if (values == null)
return null;
return ToCollection(values, collectionType, existingValue, serializer);
}
else
{
throw new JsonSerializationException("Unknown token type: " + token.ToString());
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
You need to upgrade your Owner to a LeaseOwner.
You can write a JsonConverter for this purpose that loads the relevant portion of JSON into a JObject, then checks to see whether the object looks like one from the old data model, or the new. If the JSON looks old, map fields as necessary using Linq to JSON. If the JSON object looks new, you can just populate your LeaseOwner with it.
Since you are setting PreserveReferencesHandling = PreserveReferencesHandling.Objects the converter will need to handle the "$ref" properties manually:
public class OwnerToLeaseOwnerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(LeaseOwner).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var item = JObject.Load(reader);
if (item["$ref"] != null)
{
var previous = serializer.ReferenceResolver.ResolveReference(serializer, (string)item["$ref"]);
if (previous is LeaseOwner)
return previous;
else if (previous is Owner)
{
var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
leaseOwner.Owner = (Owner)previous;
return leaseOwner;
}
else
{
throw new JsonSerializationException("Invalid type of previous object: " + previous);
}
}
else
{
var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
if (item["Name"] != null)
{
// Convert from Owner to LeaseOwner. If $id is present, this stores the reference mapping in the reference table for us.
leaseOwner.Owner = item.ToObject<Owner>(serializer);
}
else
{
// PopulateObject. If $id is present, this stores the reference mapping in the reference table for us.
item.PopulateObject(leaseOwner, serializer);
}
return leaseOwner;
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
These use the extensions:
public static class JsonExtensions
{
public static T DefaultCreate<T>(this JsonSerializer serializer, Type objectType, object existingValue)
{
if (serializer == null)
throw new ArgumentNullException();
if (existingValue is T)
return (T)existingValue;
return (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
}
public static void PopulateObject(this JToken obj, object target, JsonSerializer serializer)
{
if (target == null)
throw new NullReferenceException();
if (obj == null)
return;
using (var reader = obj.CreateReader())
serializer.Populate(reader, target);
}
}
public static class TypeExtensions
{
/// <summary>
/// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetCollectItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
public static bool IsDictionary(this Type type)
{
if (typeof(IDictionary).IsAssignableFrom(type))
return true;
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
return true;
}
}
return false;
}
}
public static class ListExtensions
{
public static bool RemoveLast<T>(this IList<T> list, T item)
{
if (list == null)
throw new ArgumentNullException();
var comparer = EqualityComparer<T>.Default;
for (int i = list.Count - 1; i >= 0; i--)
{
if (comparer.Equals(list[i], item))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
}
You can apply the converters directly to your data model using JsonConverterAttribute, like so:
public class LeaseInstrument
{
[JsonConverter(typeof(IgnoreCollectionTypeConverter), typeof(OwnerToLeaseOwnerConverter))]
public ObservableCollection<LeaseOwner> OriginalLessees { get; set; }
}
If you don't want to have a dependency on Json.NET in your data model, you can do this in your custom contract resolver:
public class WritablePropertiesOnlyResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var result = base.CreateProperty(member, memberSerialization);
if (typeof(LeaseInstrument).IsAssignableFrom(result.DeclaringType) && typeof(ICollection<LeaseOwner>).IsAssignableFrom(result.PropertyType))
{
var converter = new IgnoreCollectionTypeConverter { ItemConverterType = typeof(OwnerToLeaseOwnerConverter) };
result.Converter = result.Converter ?? converter;
result.MemberConverter = result.MemberConverter ?? converter;
}
return result;
}
}
Incidentally, you might want to cache your custom contract resolver for best performance.
You might find our library Migrations.Json.Net helpful
https://github.com/Weingartner/Migrations.Json.Net
A Simple example. Say you start with a class
public class Person {
public string Name {get;set}
}
and then you want to migrate to
public class Person {
public string FirstName {get;set}
public string SecondName {get;set}
public string Name => $"{FirstName} {SecondName}";
}
you would perhaps do the following migration
public class Person {
public string FirstName {get;set}
public string SecondName {get;set}
public string Name => $"{FirstName} {SecondName}";
public void migrate_1(JToken token, JsonSerializer s){
var name = token["Name"];
var names = names.Split(" ");
token["FirstName"] = names[0];
token["SecondName"] = names[1];
return token;
}
}
The above glosses over some details but there is a full example on the homepage of the project. We use this extensively in two of our production projects. The example on the homepage has 13 migrations to a complex object that has changed over several years.

Categories

Resources