I have a .NET worker service which will run periodically.
When the service runs, I get an object from data source with identifier
I change the above retrieved object to required format, and store this object through some service with the same identifier
When my service run next time, I get the object from data source with same identifier (object A)
I will fetch the stored object from some service with same identifier (object B)
I want to know what changed between these two objects A & B, any property change, any list item got added/updated/deleted? These objects will contain nested complex data types.
We have a working solution, using reflection, but we are not sure whether this is correct approach
I have given my existing code to track changes, I am aware this not optimized code, but before proceeding to optimize and refactor it, I want to know any other proven approach is available?
public class TrackerService
{
public T TrackChanges<T>(T newObj, T oldObj) where T : class
{
var clonedObj= ((ResourceBase)(object)oldObj).Clone();
TrackChanges(newObj, oldObj, clonedObj);
return (T)clonedObj;
}
private T TrackChanges<T>(T newObj, T oldObj, T clonedObj) where T : class
{
var oldResourceBase = (ResourceBase)(object)oldObj;
var newResourceBase = (ResourceBase)(object)newObj;
if (oldResourceBase.ResourceId != newResourceBase.ResourceId)
{
throw new Exception($"ResourceId not matching, Type: {newObj.GetType()}, Old Id {oldResourceBase.ResourceId}, New Id {newResourceBase.ResourceId}");
}
var properties = newObj.GetType().GetProperties();
foreach (var property in properties)
{
if (!property.PropertyType.IsTrackableType())
{
throw new Exception($"Property is not trackable type, Type: {property.PropertyType}, Name: {property.Name}");
}
if (property.PropertyType.IsSimpleType())
{
TrackSimplePropertyChanges(property, newObj, oldObj, clonedObj);
}
else if (property.PropertyType.IsCollection())
{
TrackCollectionPropertyChanges(property, newObj, oldObj, clonedObj);
}
else if (property.PropertyType.BaseType == typeof(ResourceBase))
{
TrackComplexPropertyChanges(property, newObj, oldObj, clonedObj);
}
}
return clonedObj;
}
private void TrackSimplePropertyChanges<T>(PropertyInfo property, T newObj, T oldObj, T clonedObj) where T : class
{
var clonedResourceBase = (ResourceBase)(object)clonedObj;
var oldPropertyObj = property.GetValue(oldObj, null);
var newPropertyObj = property.GetValue(newObj, null);
if (oldPropertyObj != null && newPropertyObj != null)
{
if (!oldPropertyObj.Equals(newPropertyObj))
{
property.SetValue(clonedObj, newPropertyObj);
clonedResourceBase.State = ResourceState.Modified;
}
}
else if (oldPropertyObj == null && newPropertyObj != null)
{
property.SetValue(clonedObj, newPropertyObj);
clonedResourceBase.State = ResourceState.Modified;
}
else if (newPropertyObj == null && oldPropertyObj != null)
{
property.SetValue(clonedObj, null);
clonedResourceBase.State = ResourceState.Modified;
}
}
private void TrackComplexPropertyChanges<T>(PropertyInfo property, T newObj, T oldObj, T clonedObj) where T : class
{
var clonedResourceBase = (ResourceBase)(object)clonedObj;
var oldPropertyObj = property.GetValue(oldObj, null);
var newPropertyObj = property.GetValue(newObj, null);
var clonedPropertyObj = property.GetValue(clonedObj, null);
if (oldPropertyObj != null && newPropertyObj != null && clonedPropertyObj != null)
{
TrackChanges(oldPropertyObj, newPropertyObj, clonedPropertyObj);
var clonedPropertyResourceBase = (ResourceBase)(object)clonedPropertyObj;
if (clonedPropertyResourceBase.State != ResourceState.UnModified)
{
clonedResourceBase.State = ResourceState.Modified;
}
}
else if (oldPropertyObj != null && clonedPropertyObj != null && newPropertyObj == null)
{
var clonedPropertyResourceBase = (ResourceBase)(object)clonedPropertyObj;
clonedPropertyResourceBase.State = ResourceState.Deleted;
clonedResourceBase.State = ResourceState.Modified;
}
else if (oldPropertyObj == null && clonedPropertyObj == null && newPropertyObj != null)
{
var newPropertyResourceBase = (ResourceBase)(object)newPropertyObj;
var clonedPropertyResourceBase = (ResourceBase)newPropertyResourceBase.Clone();
clonedPropertyResourceBase.State = ResourceState.Added;
property.SetValue(clonedObj, clonedPropertyResourceBase);
clonedResourceBase.State = ResourceState.Modified;
}
}
private void TrackCollectionPropertyChanges<T>(PropertyInfo property, T newObj, T oldObj, T clonedObj) where T : class
{
var clonedResourceBase = (ResourceBase)(object)clonedObj;
if (oldObj != null && newObj != null)
{
if (property.PropertyType.GetGenericICollectionsArgument().IsSimpleType())
{
TrackSimpleCollectionChanges(property, newObj, oldObj, clonedObj, clonedResourceBase);
}
else
{
TrackComplexCollectionChanges(property, newObj, oldObj, clonedObj, clonedResourceBase);
}
}
}
private void TrackSimpleCollectionChanges<T>(PropertyInfo property, T newObj, T oldObj, T clonedObj, ResourceBase clonedResourceBase) where T : class
{
var oldPropertyObj = (IEnumerable<ResourceBase>)property.GetValue(oldObj, null)!;
var newPropertyObj = (IEnumerable<ResourceBase>)property.GetValue(newObj, null)!;
var clonedPropertyCollection = property.GetValue(clonedObj, null)!;
var castedOldPropertyObj = oldPropertyObj.ToList();
var castedNewPropertyObj = newPropertyObj.ToList();
var tobeAdded = castedNewPropertyObj.Except(castedOldPropertyObj);
var tobeRemoved = castedOldPropertyObj.Except(castedNewPropertyObj);
foreach (var item in tobeAdded)
{
clonedPropertyCollection.GetType().GetMethod("Add")!.Invoke(clonedPropertyCollection, new[] { item });
}
foreach (var item in tobeRemoved)
{
clonedPropertyCollection.GetType().GetMethod("Remove")!.Invoke(clonedPropertyCollection, new[] { item });
}
if (tobeAdded.Count() > 0 || tobeRemoved.Count() > 0)
{
clonedResourceBase.State = ResourceState.Modified;
}
}
private void TrackComplexCollectionChanges<T>(PropertyInfo property, T newObj, T oldObj, T clonedObj, ResourceBase clonedResourceBase) where T : class
{
var oldPropertyObj = (IEnumerable<ResourceBase>)property.GetValue(oldObj, null)!;
var newPropertyObj = (IEnumerable<ResourceBase>)property.GetValue(newObj, null)!;
var clonedPropertyObj = (IEnumerable<ResourceBase>)property.GetValue(clonedObj, null)!;
var clonedPropertyCollection = property.GetValue(clonedObj, null)!;
var castedOldPropertyObj = oldPropertyObj.ToList();
var castedNewPropertyObj = newPropertyObj.ToList();
var castedClonedPropertyObj = clonedPropertyObj.ToList();
var tobeAdded = castedNewPropertyObj.Except(castedOldPropertyObj, new ResourceComparer());
var tobeRemoved = castedOldPropertyObj.Except(castedNewPropertyObj, new ResourceComparer());
foreach (ResourceBase item in tobeAdded)
{
var clonedItem = (ResourceBase)item.Clone();
clonedItem.State = ResourceState.Added;
clonedPropertyCollection.GetType().GetMethod("Add")!.Invoke(clonedPropertyCollection, new[] { clonedItem });
}
foreach (ResourceBase item in tobeRemoved)
{
ResourceBase clonedItem = castedClonedPropertyObj.First(r => r.ResourceId == item.ResourceId);
clonedItem.State = ResourceState.Deleted;
}
var matchingItem = castedNewPropertyObj.Intersect(castedOldPropertyObj, new ResourceComparer());
foreach (ResourceBase item in matchingItem)
{
var newCollectionObj = castedNewPropertyObj.First(r => r.ResourceId == item.ResourceId);
var oldCollectionObj = castedOldPropertyObj.First(r => r.ResourceId == item.ResourceId);
var clonedCollectionObj = castedClonedPropertyObj.First(r => r.ResourceId == item.ResourceId);
clonedCollectionObj = TrackChanges(newCollectionObj, oldCollectionObj, clonedCollectionObj);
if (clonedCollectionObj.State != ResourceState.UnModified)
{
clonedResourceBase.State = ResourceState.Modified;
}
}
}
}
When I run the following code, it only returns a MethodInfo/FieldInfo/etc. that belongs directly to the Type that I'm searching for the info object in. How do I find the info object regardless of the fact that it resides in a base class and could be private?
obj.GetType().GetMethod(methodName, bindingFlags);
Well, you answered your own question, but as far as I understood, you main requirement is How do I find the info object regardless of where it is found in the hierarchy?
You don't need recursion here to get all members in the full hierarchy. You can use GetMembers function on a Type and it will return all members including all base classes.
Next code example demonstrates this:
var names =
typeof(MyClass).GetMembers()
.Select (x => x.Name);
Console.WriteLine (string.Join(Environment.NewLine, names));
for such structure
class MyClass : Base
{
public string Name { get; set; }
public string Surname { get; set; }
}
class Base
{
public string Name { get; set; }
}
returns
get_Name
set_Name
get_Surname
set_Surname
get_Name
set_Name
ToString
Equals
GetHashCode
GetType
.ctor
Name
Surname
note that get_Name accessor for auto-property appears twice because MyClass hides Name property of the base class. Also note ToString, GetType and other methods, defined in object class
The following code will look through each base class of an object if the info object is not found in the child class. Note that though it will return the base class info object, it will return the object that it "runs into first", so if you have a variable called _blah in your child class and a variable called _blah in a base class, then the _blah from the child class will be returned.
public static MethodInfo GetMethodInfo(this Type objType, string methodName, BindingFlags flags, bool isFirstTypeChecked = true)
{
MethodInfo methodInfo = objType.GetMethod(methodName, flags);
if (methodInfo == null && objType.BaseType != null)
{
methodInfo = objType.BaseType.GetMethodInfo(methodName, flags, false);
}
if (methodInfo == null && isFirstTypeChecked)
{
throw new MissingMethodException(String.Format("Method {0}.{1} could not be found with the following BindingFlags: {2}", objType.ReflectedType.FullName, methodName, flags.ToString()));
}
return methodInfo;
}
public static FieldInfo GetFieldInfo(this Type objType, string fieldName, BindingFlags flags, bool isFirstTypeChecked = true)
{
FieldInfo fieldInfo = objType.GetField(fieldName, flags);
if (fieldInfo == null && objType.BaseType != null)
{
fieldInfo = objType.BaseType.GetFieldInfo(fieldName, flags, false);
}
if (fieldInfo == null && isFirstTypeChecked)
{
throw new MissingFieldException(String.Format("Field {0}.{1} could not be found with the following BindingFlags: {2}", objType.ReflectedType.FullName, fieldName, flags.ToString()));
}
return fieldInfo;
}
I altered bsara's implementation to be able to find private members.
I use it like this:
public static void Save(string filename, object obj)
{
try
{
using Stream s = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
var b = new BinaryFormatter();
b.Serialize(s, obj);
}
catch(SerializationException e)
{
var type= e.Message.Split("in Assembly")[0].Replace("Type", string.Empty).Replace("'", string.Empty).Trim();
var assembly=e.Message.Split("in Assembly")[1].Split("'")[1];
var atype= Type.GetType(type);
string path = FindObject(new Stack<object>(new object[] { obj }), atype, "[myself]");
throw new SerializationException($"Could not serialize path {path} in {obj.GetType().Name} due to not being able to process {type} from {assembly}. see inner exception for details", e);
}
}
the methods with a small update:
private static bool TrySerialize(object obj)
{
if(obj == null)
return true;
var stream = new MemoryStream();
var bf = new BinaryFormatter();
try
{
bf.Serialize(stream, obj);
}
catch(SerializationException)
{
return false;
}
return true;
}
private static string FindObject(Stack<object> self, Type typeToFind, string path)
{
var _self = self.Peek();
if(self.Where(x => x.Equals(_self)).Count() > 1) return null;
foreach(var prop in _self.GetType().GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic).Where(x => !x.GetCustomAttributes(true).Any(y => y is XmlIgnoreAttribute)))
{
switch(prop.MemberType)
{
case System.Reflection.MemberTypes.Property:
{
var line = string.Format("{0}::{1}", path, prop.Name);
var _prop = prop as PropertyInfo;
if(_prop.GetIndexParameters().Count() > 0) break;
if(typeToFind.IsAssignableFrom(_prop.PropertyType))
return line;
if(_prop.PropertyType.IsPrimitive || _prop.PropertyType == typeof(DateTime) || _prop.PropertyType == typeof(string))
continue;
var subInst = _prop.GetValue(_self, new object[0]);
if(subInst == null)
continue;
if(!TrySerialize(subInst))
{
System.Diagnostics.Debugger.Log(0, "", string.Format("Cannot serialize {0}\n", line));
}
self.Push(subInst);
var result = FindObject(self, typeToFind, line);
self.Pop();
if(result != null)
return result;
}
break;
case System.Reflection.MemberTypes.Field:
{
var line = string.Format("{0}::*{1}", path, prop.Name);
var _prop = prop as FieldInfo;
if(typeToFind.IsAssignableFrom(_prop.FieldType))
return line;
if(_prop.FieldType.IsPrimitive || _prop.FieldType == typeof(DateTime) || _prop.FieldType == typeof(string))
continue;
var subInst = _prop.GetValue(_self);
if(subInst == null)
continue;
if(!TrySerialize(subInst))
{
System.Diagnostics.Debugger.Log(0, "", string.Format("Cannot serialize field {0}\n", line));
}
self.Push(subInst);
var result = FindObject(self, typeToFind, line);
self.Pop();
if(result != null)
return result;
}
break;
case System.Reflection.MemberTypes.Event:
{
var line = string.Format("{0}::!{1}", path, prop.Name);
var _prop = prop as EventInfo;
if(typeToFind.IsAssignableFrom(_prop.EventHandlerType))
return line;
var field = _self.GetType().GetField(_prop.Name,
BindingFlags.NonPublic |BindingFlags.Instance |BindingFlags.GetField);
if(field != null && !field.GetCustomAttributes(true).Any(x => x is NonSerializedAttribute) && !TrySerialize(field.GetValue(_self)))
{
System.Diagnostics.Debugger.Log(0, "", string.Format("Cannot serialize event {0}\n", line));
}
}
break;
case System.Reflection.MemberTypes.Custom:
{
}
break;
default: break;
}
}
if(_self is IEnumerable)
{
var list = (_self as IEnumerable).Cast<object>();
var index = 0;
foreach(var item in list)
{
index++;
self.Push(item);
var result = FindObject(self, typeToFind, string.Format("{0}[{1}]", path, index));
self.Pop();
if(result != null)
return result;
}
}
return null;
}
I am working with a cut down version of C# 3.5 on a platform that doesn't have the flexibility of using third party libraries.
While I can parse JSON (using a json stream reader), I am not sure how to actually turn it into a class. (there is also no access to the usual json to class deserializer).
Does anyone know how to use reflection to manually (yet dynamically) turn a JSON string into a class?
sample Json:
{"items":[
{"firstName":"bob", "lastName":"smith", "id":1001, "foods": [{"name":"fish", "name":"bacon", "name":"cereal"}]},
{"firstName":"sarah", "lastName":"smith", "id":1002, "foods": [{"name":"bacon", "name":"apples", "name":"chocolate"}]},
{"firstName":"tom", "lastName":"waffle", "id":1003, "foods": [{"name":"waffles", "name":"sticks", "name":"stones"}]},
{"firstName":"reginald", "lastName":"hamtuft", "id":1003, "foods": [{"name":"ham", "name":"cigars", "name":"noisy children"}]}
]}
Thanks Pete and the rest for getting me on the right track for this. In my case I also had to deserialize a JSON string to a Strongly Typed object in a SQL CLR Function, so I was limited on the libraries I could use "safely" (more info here).
I modified the code for ParseJSON, which deserializes into a Dictionary<string, object> to be able to deserialize arrays of arrays, which it couldn't do, I also developed some methods to cast the resulting Dictionary into a Strongly Typed Object without using the JavaScriptConverter or the System.Runtime.Serialization library, with this code we are able to do the following:
//we have a foo and bar classes with a variety of fields and properties
private class foo
{
public List<double[][]> data;
public IEnumerable<object> DataObj;
public int integerField;
public long longProperty { get; set; }
public string stringValue;
public int? nullableInt;
public DateTime dateTimeValue;
public List<bar> classValues;
}
private class bar
{
public string stringValue;
public DateTimeOffset dateTimeOffsetValue;
}
static void Main(string[] args)
{
//lets deserialize the following JSON string into our foo object,
//the dictionary is optional, and not necessary if our JSON property names are the same as in our object.
//in this case it's used to map the "jdata" property on the JSON string to the "data" property of our object,
//in the case of the "dataObj", we are mapping to the uppercase field of our object
string JSONstring = "{\"jdata\":[[[1526518800000,7.0],[1526518834200,7.0]],[[1526549272200,25.0],[1526549306400,25.0]]],\"dataObj\":[[[1526518800000,7.0],[1526518834200,7.0]],\"abc\",123],\"integerField\":623,\"longProperty\":456789,\"stringValue\":\"foo\",\"nullableInt\":\"\",\"dateTimeValue\":\"2018-05-17T01:00:00.0000000\", \"classValues\": [{\"stringValue\":\"test\",\"dateTimeOffsetValue\":\"2018-05-17T05:00:00.0000000\"},{\"stringValue\":\"test2\",\"dateTimeOffsetValue\":\"2018-05-17T06:00:00.0000000\"}]}";
var mappingDict = new Dictionary<string, string>() { { "jdata", "data" }, { "dataObj", "DataObj" } };
foo myObject = ParseJSON<foo>(JSONstring, mappingDict);
}
The ParseJSON method will take a JSON string as input and optionally a Dictionary<string, string> and will attempt to cast it into the Type T the dictionary is used to map any property on the JSON string into a property of the object (for example, the "jdata"/"data" dictionary declared above).
public static T ParseJSON<T>(string jsonString, Dictionary<string, string> mappingTable = null)
{
Dictionary<string, object> jsonDictionary = ParseJSON(jsonString);
T castedObj = CastAs<T>(jsonDictionary, mappingTable);
return castedObj;
}
The following is my modified method for JSON parsing (can parse arrays of arrays):
public static Dictionary<string, object> ParseJSON(string json)
{
int end;
return ParseJSON(json, 0, out end);
}
private static Dictionary<string, object> ParseJSON(string json, int start, out int end)
{
Dictionary<string, object> dict = new Dictionary<string, object>();
bool escbegin = false;
bool escend = false;
bool inquotes = false;
string key = null;
int cend;
StringBuilder sb = new StringBuilder();
Dictionary<string, object> child = null;
List<object> arraylist = null;
Regex regex = new Regex(#"\\u([0-9a-z]{4})", RegexOptions.IgnoreCase);
int autoKey = 0;
int subArrayCount = 0;
List<int> arrayIndexes = new List<int>();
bool inSingleQuotes = false;
bool inDoubleQuotes = false;
for (int i = start; i < json.Length; i++)
{
char c = json[i];
if (c == '\\') escbegin = !escbegin;
if (!escbegin)
{
if (c == '"' && !inSingleQuotes)
{
inDoubleQuotes = !inDoubleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
else if (c == '\'' && !inDoubleQuotes)
{
inSingleQuotes = !inSingleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
if (!inquotes)
{
switch (c)
{
case '{':
if (i != start)
{
child = ParseJSON(json, i, out cend);
if (arraylist != null)
{
arraylist.Add(child);
}
else
{
dict.Add(key.Trim(), child);
key = null;
}
i = cend;
}
continue;
case '}':
end = i;
if (key != null)
{
if (arraylist != null) dict.Add(key.Trim(), arraylist);
else dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
}
return dict;
case '[':
if (arraylist != null)
{
List<object> _tempArrayList = arraylist;
for (int l = 0; l < subArrayCount; l++)
{
if (l == subArrayCount - 1)
{
_tempArrayList.Add(new List<object>());
}
else
{
_tempArrayList = (List<object>)_tempArrayList[arrayIndexes[l]];
}
}
if (arrayIndexes.Count < subArrayCount)
{
arrayIndexes.Add(0);
}
subArrayCount++;
}
else
{
arraylist = new List<object>();
subArrayCount++;
}
continue;
case ']':
if (key == null)
{
key = "array" + autoKey.ToString();
autoKey++;
}
if (arraylist != null)
{
List<object> _tempArrayList = arraylist;
for (int l = 0; l < subArrayCount; l++)
{
if (l == subArrayCount - 1)
{
if (sb.Length > 0)
{
_tempArrayList.Add(sb.ToString());
}
subArrayCount--;
if (subArrayCount == arrayIndexes.Count)
{
if (arrayIndexes.Count > 0)
{
arrayIndexes[arrayIndexes.Count - 1]++;
}
}
else if (subArrayCount == arrayIndexes.Count - 1)
{
arrayIndexes.RemoveAt(arrayIndexes.Count - 1);
if (arrayIndexes.Count > 0)
{
arrayIndexes[arrayIndexes.Count - 1]++;
}
}
}
else
{
_tempArrayList = (List<object>)_tempArrayList[arrayIndexes[l]];
}
}
sb.Length = 0;
}
if (subArrayCount == 0)
{
dict.Add(key.Trim(), arraylist);
arraylist = null;
key = null;
}
continue;
case ',':
if (arraylist == null && key != null)
{
dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
key = null;
sb.Length = 0;
}
if (arraylist != null && sb.Length > 0)
{
List<object> _tempArrayList = arraylist;
for (int l = 0; l < subArrayCount; l++)
{
if (l == subArrayCount - 1)
{
_tempArrayList.Add(sb.ToString());
}
else
{
_tempArrayList = (List<object>)_tempArrayList[arrayIndexes[l]];
}
}
sb.Length = 0;
}
continue;
case ':':
key = DecodeString(regex, sb.ToString());
sb.Length = 0;
continue;
}
}
}
sb.Append(c);
if (escend) escbegin = false;
if (escbegin) escend = true;
else escend = false;
}
end = json.Length - 1;
return dict; //shouldn't ever get here unless the JSON is malformed
}
private static string DecodeString(Regex regex, string str)
{
return Regex.Unescape(regex.Replace(str, match => char.ConvertFromUtf32(Int32.Parse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber))));
}
The following methods attempt to cast the returned dictionary from the previous method into a Strong Typed Object, I know is lengthy but it does the job:
private static T CastAs<T>(Dictionary<string, object> source, Dictionary<string, string> mappingTable = null)
{
T outputData = (T)Activator.CreateInstance(typeof(T));
TrySet(outputData, source, mappingTable);
return outputData;
}
private static void TrySet(object target, Dictionary<string, object> source, Dictionary<string, string> mappingTable = null)
{
if (target == null)
{
throw new ArgumentNullException("target");
}
bool useMappingTable = mappingTable != null && mappingTable.Count > 0;
foreach (KeyValuePair<string, object> kv in source)
{
string propertyName = null;
if (useMappingTable && mappingTable.ContainsKey(kv.Key))
{
propertyName = mappingTable[kv.Key];
}
else
{
propertyName = kv.Key;
}
if (!string.IsNullOrEmpty(propertyName))
{
UpdateMember(target, propertyName, kv.Value, mappingTable);
}
}
}
private static void UpdateMember(object target, string propertyName, object value, Dictionary<string, string> mappingTable)
{
try
{
FieldInfo fieldInfo = target.GetType().GetField(propertyName);
if (fieldInfo != null)
{
value = ConvertTo(value, fieldInfo.FieldType, mappingTable);
fieldInfo.SetValue(target, value);
}
else
{
PropertyInfo propInfo = target.GetType().GetProperty(propertyName);
if (propInfo != null)
{
value = ConvertTo(value, propInfo.PropertyType, mappingTable);
propInfo.SetValue(target, value);
}
}
}
catch (Exception ex)
{
throw ex;
}
}
private static object ConvertTo(object value, Type targetType, Dictionary<string, string> mappingTable)
{
try
{
bool isNullable = false;
Type sourceType = value.GetType();
//Obtain actual type to convert to (this is necessary in case of Nullable types)
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
isNullable = true;
targetType = targetType.GetGenericArguments()[0];
}
if (isNullable && string.IsNullOrWhiteSpace(Convert.ToString(value)))
{
return null;
}
//if we are converting from a dictionary to a class, call the TrySet method to convert its members
else if (targetType.IsClass && sourceType.IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
//make sure our value is actually a Dictionary<string, object> in order to be able to cast
if (sourceType.GetGenericArguments()[0] == typeof(string))
{
object convertedValue = Activator.CreateInstance(targetType);
TrySet(convertedValue, (Dictionary<string, object>)value, mappingTable);
return convertedValue;
}
return null;
}
else if (IsCollection(value))
{
Type elementType = GetCollectionElementType(targetType);
if (elementType != null)
{
if (targetType.BaseType == typeof(Array))
{
return ConvertToArray(elementType, value, mappingTable);
}
else
{
return ConvertToList(elementType, value, mappingTable);
}
}
else
{
throw new NullReferenceException();
}
}
else if (targetType == typeof(DateTimeOffset))
{
return new DateTimeOffset((DateTime)ChangeType(value, typeof(DateTime)));
}
else if (targetType == typeof(object))
{
return value;
}
else
{
return ChangeType(value, targetType);
}
}
catch (Exception ex)
{
if (targetType.IsValueType)
{
return Activator.CreateInstance(targetType);
}
return null;
}
}
private static Array ConvertToArray(Type elementType, object value, Dictionary<string, string> mappingTable)
{
Array collection = Array.CreateInstance(elementType, ((ICollection)value).Count);
int i = 0;
foreach (object item in (IEnumerable)value)
{
try
{
collection.SetValue(ConvertTo(item, elementType, mappingTable), i);
i++;
}
catch (Exception ex)
{
//nothing here, just skip the item
}
}
return collection;
}
private static IList ConvertToList(Type elementType, object value, Dictionary<string, string> mappingTable)
{
Type listType = typeof(List<>);
Type constructedListType = listType.MakeGenericType(elementType);
IList collection = (IList)Activator.CreateInstance(constructedListType);
foreach (object item in (IEnumerable)value)
{
try
{
collection.Add(ConvertTo(item, elementType, mappingTable));
}
catch (Exception ex)
{
//nothing here, just skip the item
}
}
return collection;
}
private static bool IsCollection(object obj)
{
bool isCollection = false;
Type objType = obj.GetType();
if (!typeof(string).IsAssignableFrom(objType) && typeof(IEnumerable).IsAssignableFrom(objType))
{
isCollection = true;
}
return isCollection;
}
private static Type GetCollectionElementType(Type objType)
{
Type elementType;
Type[] genericArgs = objType.GenericTypeArguments;
if (genericArgs.Length > 0)
{
elementType = genericArgs[0];
}
else
{
elementType = objType.GetElementType();
}
return elementType;
}
private static object ChangeType(object value, Type castTo)
{
try
{
return Convert.ChangeType(value, castTo);
}
catch (Exception ex)
{
//if the conversion failed, just return the original value
return value;
}
}
I hope this is helpful to anyone still looking for a way to do this.
Okay, I'm redoing my answer based on the feedback. The dynamic object generator code still comes from this:
Deserialize JSON into C# dynamic object?
This uses RegEx, Generic collections and it does use Linq, but only in 2 lines and those can easily rewritten to not use Linq (the two 'result = ' lines at the end of DynamicJsonObject.TryGetMember()). The generic dictionaries can also be replaced with hash tables if necessary.
The json parser is adapted from How can I deserialize JSON to a simple Dictionary<string,string> in ASP.NET?
class Program
{
static void Main(string[] args)
{
string data = "{ 'test': 42, 'test2': 'test2\"', 'structure' : { 'field1': 'field1', 'field2': 44 } }";
dynamic x = new DynamicJsonObject(JsonMaker.ParseJSON(data));
Console.WriteLine(x.test2);
Console.WriteLine(x.structure.field1);
Console.ReadLine();
}
}
public class DynamicJsonObject : DynamicObject
{
private readonly IDictionary<string, object> _dictionary;
public DynamicJsonObject(IDictionary<string, object> dictionary)
{
if (dictionary == null)
throw new ArgumentNullException("dictionary");
_dictionary = dictionary;
}
public override string ToString()
{
var sb = new StringBuilder();
ToString(sb);
return sb.ToString();
}
private void ToString(StringBuilder sb)
{
sb.Append("{");
var firstInDictionary = true;
foreach (var pair in _dictionary)
{
if (!firstInDictionary)
sb.Append(",");
firstInDictionary = false;
var value = pair.Value;
var name = pair.Key;
if (value is string)
{
sb.AppendFormat("\"{0}\":\"{1}\"", name, value);
}
else if (value is IDictionary<string, object>)
{
sb.AppendFormat("\"{0}\":", name);
new DynamicJsonObject((IDictionary<string, object>)value).ToString(sb);
}
else if (value is ArrayList)
{
sb.Append("\"");
sb.Append(name);
sb.Append("\":[");
var firstInArray = true;
foreach (var arrayValue in (ArrayList)value)
{
if (!firstInArray)
sb.Append(",");
firstInArray = false;
if (arrayValue is IDictionary<string, object>)
new DynamicJsonObject((IDictionary<string, object>)arrayValue).ToString(sb);
else if (arrayValue is string)
sb.AppendFormat("\"{0}\"", arrayValue);
else
sb.AppendFormat("{0}", arrayValue);
}
sb.Append("]");
}
else
{
sb.AppendFormat("\"{0}\":{1}", name, value);
}
}
sb.Append("}");
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (!_dictionary.TryGetValue(binder.Name, out result))
{
// return null to avoid exception. caller can check for null this way...
result = null;
return true;
}
var dictionary = result as IDictionary<string, object>;
if (dictionary != null)
{
result = new DynamicJsonObject(dictionary);
return true;
}
var arrayList = result as ArrayList;
if (arrayList != null && arrayList.Count > 0)
{
if (arrayList[0] is IDictionary<string, object>)
result = new List<object>(arrayList.Cast<IDictionary<string, object>>().Select(x => new DynamicJsonObject(x)));
else
result = new List<object>(arrayList.Cast<object>());
}
return true;
}
}
public static class JsonMaker
{
public static Dictionary<string, object> ParseJSON(string json)
{
int end;
return ParseJSON(json, 0, out end);
}
private static Dictionary<string, object> ParseJSON(string json, int start, out int end)
{
Dictionary<string, object> dict = new Dictionary<string, object>();
bool escbegin = false;
bool escend = false;
bool inquotes = false;
string key = null;
int cend;
StringBuilder sb = new StringBuilder();
Dictionary<string, object> child = null;
List<object> arraylist = null;
Regex regex = new Regex(#"\\u([0-9a-z]{4})", RegexOptions.IgnoreCase);
int autoKey = 0;
bool inSingleQuotes = false;
bool inDoubleQuotes = false;
for (int i = start; i < json.Length; i++)
{
char c = json[i];
if (c == '\\') escbegin = !escbegin;
if (!escbegin)
{
if (c == '"' && !inSingleQuotes)
{
inDoubleQuotes = !inDoubleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
else if (c == '\'' && !inDoubleQuotes)
{
inSingleQuotes = !inSingleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
if (!inquotes)
{
switch (c)
{
case '{':
if (i != start)
{
child = ParseJSON(json, i, out cend);
if (arraylist != null) arraylist.Add(child);
else
{
dict.Add(key.Trim(), child);
key = null;
}
i = cend;
}
continue;
case '}':
end = i;
if (key != null)
{
if (arraylist != null) dict.Add(key.Trim(), arraylist);
else dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
}
return dict;
case '[':
arraylist = new List<object>();
continue;
case ']':
if (key == null)
{
key = "array" + autoKey.ToString();
autoKey++;
}
if (arraylist != null && sb.Length > 0)
{
arraylist.Add(sb.ToString());
sb.Length = 0;
}
dict.Add(key.Trim(), arraylist);
arraylist = null;
key = null;
continue;
case ',':
if (arraylist == null && key != null)
{
dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
key = null;
sb.Length = 0;
}
if (arraylist != null && sb.Length > 0)
{
arraylist.Add(sb.ToString());
sb.Length = 0;
}
continue;
case ':':
key = DecodeString(regex, sb.ToString());
sb.Length = 0;
continue;
}
}
}
sb.Append(c);
if (escend) escbegin = false;
if (escbegin) escend = true;
else escend = false;
}
end = json.Length - 1;
return dict; //theoretically shouldn't ever get here
}
private static string DecodeString(Regex regex, string str)
{
return Regex.Unescape(regex.Replace(str, match => char.ConvertFromUtf32(Int32.Parse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber))));
}
}
Thanks again to Pete and the other guys for their brilliant post. I have wrapped mine around a SQL Server CLR scalar function which was incredibly useful in interrogating JSON stored in relational tables (I know some would say just use MongoDB!).
Please see below:
public class JsonHelper
{
/// <summary>
/// Parses the JSON.
/// Thanks to http://stackoverflow.com/questions/14967618/deserialize-json-to-class-manually-with-reflection
/// </summary>
/// <param name="json">The json.</param>
/// <returns></returns>
public static Dictionary<string, object> DeserializeJson(string json)
{
int end;
return DeserializeJson(json, 0, out end);
}
/// <summary>
/// Parses the JSON.
/// </summary>
/// <param name="json">The json.</param>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns></returns>
private static Dictionary<string, object> DeserializeJson(string json, int start, out int end)
{
var dict = new Dictionary<string, object>();
var escbegin = false;
var escend = false;
var inquotes = false;
string key = null;
var sb = new StringBuilder();
List<object> arraylist = null;
var regex = new Regex(#"\\u([0-9a-z]{4})", RegexOptions.IgnoreCase);
var autoKey = 0;
var inSingleQuotes = false;
var inDoubleQuotes = false;
for (var i = start; i < json.Length; i++)
{
var c = json[i];
if (c == '\\') escbegin = !escbegin;
if (!escbegin)
{
if (c == '"' && !inSingleQuotes)
{
inDoubleQuotes = !inDoubleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
if (c == '\'' && !inDoubleQuotes)
{
inSingleQuotes = !inSingleQuotes;
inquotes = !inquotes;
if (!inquotes && arraylist != null)
{
arraylist.Add(DecodeString(regex, sb.ToString()));
sb.Length = 0;
}
continue;
}
if (!inquotes)
{
switch (c)
{
case '{':
if (i != start)
{
int cend;
var child = DeserializeJson(json, i, out cend);
if (arraylist != null)
{
arraylist.Add(child);
}
else
{
dict.Add(key.Trim(), child);
key = null;
}
i = cend;
}
continue;
case '}':
end = i;
if (key != null)
{
if (arraylist != null) dict.Add(key.Trim(), arraylist);
else dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
}
return dict;
case '[':
arraylist = new List<object>();
continue;
case ']':
if (key == null)
{
key = "array" + autoKey;
autoKey++;
}
if (arraylist != null && sb.Length > 0)
{
arraylist.Add(sb.ToString());
sb.Length = 0;
}
dict.Add(key.Trim(), arraylist);
arraylist = null;
key = null;
continue;
case ',':
if (arraylist == null && key != null)
{
dict.Add(key.Trim(), DecodeString(regex, sb.ToString().Trim()));
key = null;
sb.Length = 0;
}
if (arraylist != null && sb.Length > 0)
{
arraylist.Add(sb.ToString());
sb.Length = 0;
}
continue;
case ':':
key = DecodeString(regex, sb.ToString());
sb.Length = 0;
continue;
}
}
}
sb.Append(c);
if (escend) escbegin = false;
escend = escbegin;
}
end = json.Length - 1;
return dict; // theoretically shouldn't ever get here
}
/// <summary>
/// Decodes the string.
/// </summary>
/// <param name="regex">The regex.</param>
/// <param name="str">The STR.</param>
/// <returns></returns>
private static string DecodeString(Regex regex, string str)
{
return
Regex.Unescape(regex.Replace(str,
match =>
char.ConvertFromUtf32(Int32.Parse(match.Groups[1].Value,
System.Globalization.NumberStyles
.HexNumber))));
}
/// <summary>
/// Returns true if string has an "appearance" of being JSON-like
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool IsJson(string input)
{
input = input.Trim();
return input.StartsWith("{") && input.EndsWith("}")
|| input.StartsWith("[") && input.EndsWith("]");
}
}
The CLR function is below:
/// <summary>
/// Json "extractor" capable of extracting a value of a key using the object notation.
/// </summary>
/// <param name="json"></param>
/// <param name="key"></param>
/// <returns></returns>
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlString fn_GetKeyValue(SqlString json, SqlString key)
{
var jsonString = json.ToString();
// Return if N/A
if (string.IsNullOrEmpty(jsonString) || !JsonHelper.IsJson(jsonString))
{
return json;
}
var keyString = key.ToString();
var jsonDictionary = JsonHelper.DeserializeJson(jsonString);
var lastNode = string.Empty;
foreach (var node in keyString.Split('.'))
{
if (!jsonDictionary.ContainsKey(node)) continue;
var childDictionary = jsonDictionary[node] as Dictionary<string, object>;
if (childDictionary != null)
{
jsonDictionary = childDictionary;
}
lastNode = node;
}
if (!jsonDictionary.ContainsKey(lastNode))
{
return null;
}
var keyValueString = jsonDictionary[lastNode].ToString();
return keyValueString == "null" ? null : new SqlString(keyValueString);
}
Usage would be:
-- Example 1 (querying a parent node)
SELECT dbo.fn_GetKeyValue('{
"ExchangeRates": {
"GBP": "1.2",
"USD": "2.0"
},
"Timestamp": "2015-04-10"
}', 'Timestamp');
-- Example 2 (querying a child node using a dot notation)
SELECT dbo.fn_GetKeyValue('{
"ExchangeRates": {
"GBP": "1.2",
"USD": "2.0"
},
"Timestamp": "2015-04-10"
}', 'ExchangeRates.GBP');